diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index fda7baf86..fcfbc5dbd 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -21,7 +21,7 @@ jobs: run: ./scripts/ci-build.sh 2>&1 - name: Upload micro-controller data dictionaries - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: data-dict path: ci_build/dict diff --git a/README.md b/README.md index 32fe7331a..f8e8c9590 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,14 @@ Welcome to the Klipper project! https://www.klipper3d.org/ -Klipper is a 3d-Printer firmware. It combines the power of a general -purpose computer with one or more micro-controllers. See the +The Klipper firmware controls 3d-Printers. It combines the power of a +general purpose computer with one or more micro-controllers. See the [features document](https://www.klipper3d.org/Features.html) for more -information on why you should use Klipper. +information on why you should use the Klipper software. -To begin using Klipper start by -[installing](https://www.klipper3d.org/Installation.html) it. +Start by [installing Klipper software](https://www.klipper3d.org/Installation.html). -Klipper is Free Software. See the [license](COPYING) or read the -[documentation](https://www.klipper3d.org/Overview.html). We depend on -the generous support from our +Klipper software is Free Software. See the [license](COPYING) or read +the [documentation](https://www.klipper3d.org/Overview.html). We +depend on the generous support from our [sponsors](https://www.klipper3d.org/Sponsors.html). diff --git a/config/generic-creality-v4.2.7.cfg b/config/generic-creality-v4.2.7.cfg index 21e99da59..6b8644c5e 100644 --- a/config/generic-creality-v4.2.7.cfg +++ b/config/generic-creality-v4.2.7.cfg @@ -95,4 +95,4 @@ max_z_accel: 100 aliases: EXP1_1=PC6,EXP1_3=PB10,EXP1_5=PB14,EXP1_7=PB12,EXP1_9=, EXP1_2=PB2,EXP1_4=PB11,EXP1_6=PB13,EXP1_8=PB15,EXP1_10=<5V>, - PROBE_IN=PB0,PROBE_OUT=PB1,FIL_RUNOUT=PC6 + PROBE_IN=PB0,PROBE_OUT=PB1,FIL_RUNOUT=PA4 diff --git a/config/printer-geeetech-A10T-A20T-2021.cfg b/config/printer-geeetech-A10T-A20T-2021.cfg new file mode 100644 index 000000000..7a2fb6ee9 --- /dev/null +++ b/config/printer-geeetech-A10T-A20T-2021.cfg @@ -0,0 +1,256 @@ +# This file contains common pin mappings for the Geeetech GT2560 v4.0 and v4.1b +# boards. These boards use a firmware compiled for the AVR atmega2560. +# For default Geeetech A10/A20 (1 extruder), +# A10M/A20M (mixing 2 in 1 out), +# A10T/A20T (mixing 3 in 1 out) printers +# Installation: https://www.klipper3d.org/Installation.html +# Always read for first start: https://www.klipper3d.org/Config_checks.html + +[mcu] +# Might need to be changed: https://www.klipper3d.org/Installation.html +serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 + +[printer] +kinematics: cartesian +max_velocity: 200 +max_accel: 1500 +max_z_velocity: 20 +max_z_accel: 500 + +# # uncomment for BLTouch/3DTouch +# [bltouch] +# sensor_pin: PC7 # there is an external pull up so no need in ^ +# control_pin: PB5 +# speed: 3.0 +# samples: 2 +# x_offset: -42.0 +# y_offset: -1.0 +# z_offset: 1.0 # during calibration this line is commented out and new record added at the end of file + +[safe_z_home] +home_xy_position: 100, 100 # Change coordinates to the center of your print bed +speed: 50 +z_hop: 10 # Move up 10mm +z_hop_speed: 5 + +[stepper_x] +enable_pin: !PC2 +dir_pin: !PG2 +step_pin: PC0 +microsteps: 16 +rotation_distance: 40 +endstop_pin: !PA2 # there are external pull ups +position_endstop: 0 +position_max: 220 # for A10/M/T / change to 250 for A20/M/T +homing_speed: 40 + +[stepper_y] +enable_pin: !PA7 +dir_pin: !PC4 +step_pin: PC6 +microsteps: 16 +rotation_distance: 40 +endstop_pin: !PA6 # there are external pull ups +position_endstop: 0 +position_max: 220 # for A10/M/T / change to 250 for A20/M/T +homing_speed: 40 + +[stepper_z] +enable_pin: !PA5 +dir_pin: PA1 +step_pin: PA3 +microsteps: 16 +rotation_distance: 8 +#endstop_pin: probe:z_virtual_endstop # uncomment for BLTouch/3DTouch +endstop_pin: !PC7 # comment for BLTouch/3DTouch +position_endstop: 0 # comment for BLTouch/3DTouch +position_max: 230 # for A10/M/T / change to 250 for A20/M/T +position_min: -5 +homing_speed: 20 + +[extruder] +enable_pin: !PB6 +dir_pin: PL5 +step_pin: PL3 +microsteps: 16 +rotation_distance: 8 # Needs to be optimized: https://www.klipper3d.org/Rotation_Distance.html#calibrating-rotation_distance-on-extruders +nozzle_diameter: 0.4 +filament_diameter: 1.750 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK3 +min_temp: 0 +max_temp: 250 +max_extrude_only_distance: 200.0 +# Parameters for stock hotend on A10M +# Please recalibrate according to https://www.klipper3d.org/Config_checks.html#calibrate-pid-settings +control: pid +pid_kp: 54.722 +pid_ki: 4.800 +pid_kd: 155.958 + +[extruder_stepper extruder_1] +extruder: +enable_pin: !PL1 +dir_pin: PL2 +step_pin: PL0 +microsteps: 16 +rotation_distance: 8 # Needs to be optimized: https://www.klipper3d.org/Rotation_Distance.html#calibrating-rotation_distance-on-extruders + +[extruder_stepper extruder_2] +extruder: +enable_pin: !PG0 +dir_pin: PL4 +step_pin: PL6 +microsteps: 16 +rotation_distance: 8 # Needs to be optimized: https://www.klipper3d.org/Rotation_Distance.html#calibrating-rotation_distance-on-extruders + +[heater_bed] +heater_pin: PG5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK2 +min_temp: 0 +max_temp: 120 +# Parameters for `SuperPlate` on A10M +# Please recalibrate according to https://www.klipper3d.org/Config_checks.html#calibrate-pid-settings +control: pid +pid_kp: 70.936 +pid_ki: 1.785 +pid_kd: 704.924 + +[fan] +pin: PH6 +cycle_time: 0.150 +kick_start_time: 0.300 + +# # for GT2560V4.0 with 20pin flat cable toward the display +# [display] +# lcd_type: hd44780 +# hd44780_protocol_init: True +# rs_pin: PD1 +# e_pin: PH0 +# d4_pin: PH1 +# d5_pin: PD0 +# d6_pin: PE3 +# d7_pin: PC1 +# encoder_pins: ^PG1, ^PL7 +# click_pin: ^!PD2 + + +# for GT2560V4.1B with 12pin flat cable toward the display YHCB2004-06 ver3.0 +# the aip31068_spi driver was added to Klipper on 2024-12-02, commit aecb29d2 +[display] +lcd_type: aip31068_spi +latch_pin: PE3 +spi_software_sclk_pin: PD0 +spi_software_mosi_pin: PC1 +spi_software_miso_pin: PH7 # any unused pin +encoder_pins: ^PH0, ^PH1 +click_pin: ^!PD2 + + +[filament_switch_sensor sensor_e0] +switch_pin: !PK4 + +[filament_switch_sensor sensor_e1] +switch_pin: !PK5 + +[filament_switch_sensor sensor_e2] +# switch_pin: !PE2 # for GT2560V4.0 +switch_pin: !PF0 # for GT2560V4.1B + +# to enable M118 echo command +[respond] + +# Specific macros for mixing colors. +# Add in slicer new filament color and in filament start G-Code add desired mixing factor: +# M163 S0 P50 ; set extruder 0 to 50% +# M163 S1 P40 ; set extruder 1 to 40% +# M163 S2 P10 ; set extruder 2 to 10% +# M164 ; commit the mix factors +[gcode_macro M163] +description: M163 [P] [S] Set a single mix factor (in proportion to the sum total of all mix factors). The mix must be committed to a virtual tool by M164 before it takes effect. +gcode: + {% if 'P' in params %} + {% set s = params.S|default(0)| int %} + {% if s == 0 %} + SET_GCODE_VARIABLE MACRO=M164 VARIABLE=e0_parts VALUE={params.P|default(0)|float} + M118 Set Mixing factor for extruder 0 to {params.P|default(0)|float} + {% elif s == 1 %} + SET_GCODE_VARIABLE MACRO=M164 VARIABLE=e1_parts VALUE={params.P|default(0)|float} + M118 Set Mixing factor for extruder 1 to {params.P|default(0)|float} + {% elif s == 2 %} + SET_GCODE_VARIABLE MACRO=M164 VARIABLE=e2_parts VALUE={params.P|default(0)|float} + M118 Set Mixing factor for extruder 2 to {params.P|default(0)|float} + {% endif %} + {% else %} + M118 No Mixing factor set, missing value for P + {% endif %} + M118 {e0_parts} {e1_parts} {e2_parts} + + +[gcode_macro M164] +description: Applies the set mixing factors to the extruders +# default values: +variable_e0_parts : 100 +variable_e1_parts : 0 +variable_e2_parts : 0 +gcode: + # normalize the parts to sum of 1 + {% set e0 = e0_parts / (e0_parts + e1_parts + e2_parts) | float %} + {% set e1 = e1_parts / (e0_parts + e1_parts + e2_parts) | float %} + {% set e2 = e2_parts / (e0_parts + e1_parts + e2_parts) | float %} + M118 scaled rot-dist_e0 { printer.configfile.settings.extruder.rotation_distance / (e0 + 0.000001) | float } + M118 scaled rot-dist_e1 { printer.configfile.settings['extruder_stepper extruder_1'].rotation_distance / (e1 + 0.000001) | float } + M118 scaled rot-dist_e2 { printer.configfile.settings['extruder_stepper extruder_2'].rotation_distance / (e2 + 0.000001) |float } + # activate stepper percentages + SYNC_EXTRUDER_MOTION EXTRUDER=extruder MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder_1 MOTION_QUEUE=extruder + SYNC_EXTRUDER_MOTION EXTRUDER=extruder_2 MOTION_QUEUE=extruder + SET_EXTRUDER_ROTATION_DISTANCE EXTRUDER=extruder DISTANCE={ printer.configfile.settings.extruder.rotation_distance / (e0+0.000001)|float } + SET_EXTRUDER_ROTATION_DISTANCE EXTRUDER=extruder_1 DISTANCE={ printer.configfile.settings['extruder_stepper extruder_1'].rotation_distance / (e1+0.000001)|float } + SET_EXTRUDER_ROTATION_DISTANCE EXTRUDER=extruder_2 DISTANCE={ printer.configfile.settings['extruder_stepper extruder_2'].rotation_distance / (e2+0.000001)|float } + M118 Mixing factors {e0} {e1} {e2} are activated + +# In PrusaSlicer: +# - you can add as many extruders as mixing ratios you want +# - in Printer Settings -> Custom G-code -> Tool change G-code add: +# TOOL_CHANGE EXTRUDER={next_extruder} +# - in this config file add: +# [gcode_macro TOOL_CHANGE] +# description: Tool change macro with mix ratio setup for 11 extruders +# variable_extruder: 0 +# gcode: +# {% set extruder = params.EXTRUDER|default(0)| int %} +# {% if extruder == 0 %} +# M163 S0 P100 +# M163 S1 P0 +# M163 S2 P0 +# M164 +# M118 Switching to Extruder 0 +# {% elif extruder == 1 %} +# M163 S0 P90 +# M163 S1 P10 +# M163 S2P0 +# M164 +# M118 Switching to Extruder 1 +# {% elif extruder == 2 %} +# # and so on ... +# {% else %} +# M118 Unknown extruder number: {extruder} +# {% endif %} + +# In OrcaSlicer: +# you can add as many filaments as mixing ratios you want +# in Material settings -> Advanced -> Filament start G-code add desired mixing ratio: +# ; filament start gcode +# M163 S0 P100 ; set extruder 0 +# M163 S1 P0 ; set extruder 1 +# M163 S2 P0 ; set extruder 2 +# M164 ; commit the mix factors + +# For gradient over Z axis: +# In `Printer -> Custom G-code -> After layer change G-code` add: +# M163 S0 P{ layer_num * 100 / total_layer_count } ; Gradient 0-100 +# M163 S1 P{(total_layer_count-layer_num) * 100 / total_layer_count} ; Gradient 100-0 +# M164 ; commit the mix factors diff --git a/docs/Code_Overview.md b/docs/Code_Overview.md index 0e4836acf..9ccaa60be 100644 --- a/docs/Code_Overview.md +++ b/docs/Code_Overview.md @@ -359,10 +359,10 @@ Useful steps: be efficient as it is typically only called during homing and probing operations. 5. Other methods. Implement the `check_move()`, `get_status()`, - `get_steppers()`, `home()`, and `set_position()` methods. These - functions are typically used to provide kinematic specific checks. - However, at the start of development one can use boiler-plate code - here. + `get_steppers()`, `home()`, `clear_homing_state()`, and `set_position()` + methods. These functions are typically used to provide kinematic + specific checks. However, at the start of development one can use + boiler-plate code here. 6. Implement test cases. Create a g-code file with a series of moves that can test important cases for the given kinematics. Follow the [debugging documentation](Debugging.md) to convert this g-code file diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index a73a0c37f..adc03a082 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,10 @@ All dates in this document are approximate. ## Changes +20250131: Option `VARIABLE=` in `SAVE_VARIABLE` requires lowercase +value. For example, `extruder` instead of mixedcase `Extruder` or +uppercase `EXTRUDER`. Using any uppercase letter will raise an error. + 20241203: The resonance test has been changed to include slow sweeping moves. This change requires that testing point(s) have some clearance in X/Y plane (+/- 30 mm from the test point should suffice when using diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 0b44aaa4a..e8f3353fd 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3846,6 +3846,7 @@ run_current: #driver_SEIMIN: 0 #driver_SFILT: 0 #driver_SG4_ANGLE_OFFSET: 1 +#driver_SLOPE_CONTROL: 0 # Set the given register during the configuration of the TMC2240 # chip. This may be used to set custom motor parameters. The # defaults for each parameter are next to the parameter name in the @@ -4950,6 +4951,50 @@ vssa_pin: # noise. The default is 2 seconds. ``` +### [ads1x1x] + +ADS1013, ADS1014, ADS1015, ADS1113, ADS1114 and ADS1115 are I2C based Analog to +Digital Converters that can be used for temperature sensors. They provide 4 +analog input pins either as single line or as differential input. + +Note: Use caution if using this sensor to control heaters. The heater min_temp +and max_temp are only verified in the host and only if the host is running and +operating normally. (ADC inputs directly connected to the micro-controller +verify min_temp and max_temp within the micro-controller and do not require a +working connection to the host.) + +``` +[ads1x1x my_ads1x1x] +chip: ADS1115 +#pga: 4.096V +# Default value is 4.096V. The maximum voltage range used for the input. This +# scales all values read from the ADC. Options are: 6.144V, 4.096V, 2.048V, +# 1.024V, 0.512V, 0.256V +#adc_voltage: 3.3 +# The suppy voltage for the device. This allows additional software scaling +# for all values read from the ADC. +i2c_mcu: host +i2c_bus: i2c.1 +#address_pin: GND +# Default value is GND. There can be up to four addressed devices depending +# upon wiring of the device. Check the datasheet for details. The i2c_address +# can be specified directly instead of using the address_pin. +``` + +The chip provides pins that can be used on other sensors. + +``` +sensor_type: ... +# Can be any thermistor or adc_temperature. +sensor_pin: my_ads1x1x:AIN0 +# A combination of the name of the ads1x1x chip and the pin. Possible +# pin values are AIN0, AIN1, AIN2 and AIN3 for single ended lines and +# DIFF01, DIFF03, DIFF13 and DIFF23 for differential between their +# correspoding lines. For example +# DIFF03 measures the differential between line 0 and 3. Only specific +# combinations for the differentials are allowed. +``` + ### [replicape] Replicape support - see the [beaglebone guide](Beaglebone.md) and the diff --git a/docs/Contact.md b/docs/Contact.md index 714c36bd4..c2512be1e 100644 --- a/docs/Contact.md +++ b/docs/Contact.md @@ -132,3 +132,10 @@ There are several you have questions on the code then you can also ask in the [Klipper Discourse Forum](#discourse-forum) or on the [Klipper Discord Chat](#discord-chat). + +## Professional Services + +![](img/klipper-logo-small.png) + +Custom software development, software support, and solutions: +[https://ko-fi.com/koconnor](https://ko-fi.com/koconnor) diff --git a/docs/G-Codes.md b/docs/G-Codes.md index d44017dea..78c1c74b1 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -585,15 +585,18 @@ state; issue a G28 afterwards to reset the kinematics. This command is intended for low-level diagnostics and debugging. #### SET_KINEMATIC_POSITION -`SET_KINEMATIC_POSITION [X=] [Y=] [Z=]`: Force -the low-level kinematic code to believe the toolhead is at the given -cartesian position. This is a diagnostic and debugging command; use -SET_GCODE_OFFSET and/or G92 for regular axis transformations. If an -axis is not specified then it will default to the position that the -head was last commanded to. Setting an incorrect or invalid position -may lead to internal software errors. This command may invalidate -future boundary checks; issue a G28 afterwards to reset the -kinematics. +`SET_KINEMATIC_POSITION [X=] [Y=] [Z=] +[CLEAR=<[X][Y][Z]>]`: Force the low-level kinematic code to believe the +toolhead is at the given cartesian position. This is a diagnostic and +debugging command; use SET_GCODE_OFFSET and/or G92 for regular axis +transformations. If an axis is not specified then it will default to the +position that the head was last commanded to. Setting an incorrect or +invalid position may lead to internal software errors. Use the CLEAR +parameter to forget the homing state for the given axes. Note that CLEAR +will not override the previous functionality; if an axis is not specified +to CLEAR it will have its kinematic position set as per above. This +command may invalidate future boundary checks; issue a G28 afterwards to +reset the kinematics. ### [gcode] @@ -1198,8 +1201,9 @@ has been enabled. #### SAVE_VARIABLE `SAVE_VARIABLE VARIABLE= VALUE=`: Saves the variable to -disk so that it can be used across restarts. All stored variables are -loaded into the `printer.save_variables.variables` dict at startup and +disk so that it can be used across restarts. The VARIABLE must be lowercase. +All stored variables are loaded into the +`printer.save_variables.variables` dict at startup and can be used in gcode macros. The provided VALUE is parsed as a Python literal. diff --git a/docs/Installation.md b/docs/Installation.md index ca64259aa..06550a028 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -61,7 +61,7 @@ their own Klipper-centric images. The two main Moonraker based front ends are [Fluidd](https://docs.fluidd.xyz/) and [Mainsail](https://docs.mainsail.xyz/), the latter of which has a premade install -image ["MainsailOS"](http://docs.mainsailOS.xyz), this has the option for Raspberry Pi +image ["MainsailOS"](https://docs-os.mainsail.xyz/), this has the option for Raspberry Pi and some OrangePi varianta. Fluidd can be installed via KIAUH(Klipper Install And Update Helper), which diff --git a/docs/Pressure_Advance.md b/docs/Pressure_Advance.md index 2f3601cab..1e48f26c6 100644 --- a/docs/Pressure_Advance.md +++ b/docs/Pressure_Advance.md @@ -22,7 +22,7 @@ Use a slicer to generate g-code for the large hollow square found in [docs/prints/square_tower.stl](prints/square_tower.stl). Use a high speed (eg, 100mm/s), zero infill, and a coarse layer height (the layer height should be around 75% of the nozzle diameter). Make sure any -"dynamic acceleration control" is disabled in the slicer. +"dynamic acceleration control" and "scarf joint" seams are disabled in the slicer. Prepare for the test by issuing the following G-Code command: ``` diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md index 66d840d16..ee8099025 100644 --- a/docs/Status_Reference.md +++ b/docs/Status_Reference.md @@ -39,6 +39,27 @@ the following strings: "adjust", "fine". - `current_screw`: The index for the current screw being adjusted. - `accepted_screws`: The number of accepted screws. +## canbus_stats + +The following information is available in the `canbus_stats +some_mcu_name` object (this object is automatically available if an +mcu is configured to use canbus): +- `rx_error`: The number of receive errors detected by the + micro-controller canbus hardware. +- `tx_error`: The number of transmit errors detected by the + micro-controller canbus hardware. +- `tx_retries`: The number of transmit attempts that were retried due + to bus contention or errors. +- `bus_state`: The status of the interface (typically "active" for a + bus in normal operation, "warn" for a bus with recent errors, + "passive" for a bus that will no longer transmit canbus error + frames, or "off" for a bus that will no longer transmit or receive + messages). + +Note that only the rp2XXX micro-controllers report a non-zero +`tx_retries` field and the rp2XXX micro-controllers always report +`tx_error` as zero and `bus_state` as "active". + ## configfile The following information is available in the `configfile` object diff --git a/docs/_klipper3d/mkdocs-requirements.txt b/docs/_klipper3d/mkdocs-requirements.txt index 96bf60051..20f1071c3 100644 --- a/docs/_klipper3d/mkdocs-requirements.txt +++ b/docs/_klipper3d/mkdocs-requirements.txt @@ -1,5 +1,5 @@ # Python virtualenv module requirements for mkdocs -jinja2==3.1.4 +jinja2==3.1.5 mkdocs==1.2.4 mkdocs-material==8.1.3 mkdocs-simple-hooks==0.1.3 diff --git a/docs/index.md b/docs/index.md index 17192742b..4846f96d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,13 +6,16 @@ title: Welcome ![](img/klipper-logo.png){ .center-image } -Klipper is a 3d-Printer firmware. It combines the power of a general -purpose computer with one or more micro-controllers. See the -[features](Features.md) document for more information on why you -should use Klipper. +The Klipper firmware controls 3d-Printers. It combines the power of a +general purpose computer with one or more micro-controllers. See the +[features document](https://www.klipper3d.org/Features.html) for more +information on why you should use the Klipper software. -To begin using Klipper start by [installing](Installation.md) it. +Start by [installing Klipper software](https://www.klipper3d.org/Installation.html). -Klipper is Free Software. Read the [documentation](Overview.md) or -view [the Klipper code on github](https://github.com/Klipper3d/klipper). -We depend on the generous support from our [sponsors](Sponsors.md). +Klipper software is Free Software. Read the +[documentation](https://www.klipper3d.org/Overview.html), see the +[license](COPYING), or +[download](https://github.com/Klipper3d/Klipper) the software. We +depend on the generous support from our +[sponsors](https://www.klipper3d.org/Sponsors.html). diff --git a/klippy/extras/ads1x1x.py b/klippy/extras/ads1x1x.py new file mode 100644 index 000000000..1d72996f3 --- /dev/null +++ b/klippy/extras/ads1x1x.py @@ -0,0 +1,393 @@ +# Support for I2C based ADS1013, ADS1014, ADS1015, ADS1113, ADS1114 and ADS1115 +# +# Copyright (C) 2024 Konstantin Koch +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging +import pins +from . import bus + +# Supported chip types +ADS1X1X_CHIP_TYPE = { + 'ADS1013': 3, + 'ADS1014': 4, + 'ADS1015': 5, + 'ADS1113': 13, + 'ADS1114': 14, + 'ADS1115': 15 +} + +def isADS101X(chip): + return (chip == ADS1X1X_CHIP_TYPE['ADS1013'] \ + or chip == ADS1X1X_CHIP_TYPE['ADS1014'] \ + or chip == ADS1X1X_CHIP_TYPE['ADS1015']) + +def isADS111X(chip): + return (chip == ADS1X1X_CHIP_TYPE['ADS1113'] \ + or chip == ADS1X1X_CHIP_TYPE['ADS1114'] \ + or chip == ADS1X1X_CHIP_TYPE['ADS1115']) + +# Address is defined by how the address pin is wired +ADS1X1X_CHIP_ADDR = { + 'GND': 0x48, + 'VCC': 0x49, + 'SDA': 0x4a, + 'SCL': 0x4b +} + +# Chip "pointer" registers +ADS1X1X_REG_POINTER_MASK = 0x03 +ADS1X1X_REG_POINTER = { + 'CONVERSION': 0x00, + 'CONFIG': 0x01, + 'LO_THRESH': 0x02, + 'HI_THRESH': 0x03 +} + +# Config register masks +ADS1X1X_REG_CONFIG = { + 'OS_MASK': 0x8000, + 'MULTIPLEXER_MASK': 0x7000, + 'PGA_MASK': 0x0E00, + 'MODE_MASK': 0x0100, + 'DATA_RATE_MASK': 0x00E0, + 'COMPARATOR_MODE_MASK': 0x0010, + 'COMPARATOR_POLARITY_MASK': 0x0008, + # Determines if ALERT/RDY pin latches once asserted + 'COMPARATOR_LATCHING_MASK': 0x0004, + 'COMPARATOR_QUEUE_MASK': 0x0003 +} + +# +# The following enums are to be used with the configuration functions. +# +ADS1X1X_OS = { + 'OS_IDLE': 0x8000, # Device is not performing a conversion + 'OS_SINGLE': 0x8000 # Single-conversion +} + +ADS1X1X_MUX = { + 'DIFF01': 0x0000, # Differential P = AIN0, N = AIN1 0 + 'DIFF03': 0x1000, # Differential P = AIN0, N = AIN3 4096 + 'DIFF13': 0x2000, # Differential P = AIN1, N = AIN3 8192 + 'DIFF23': 0x3000, # Differential P = AIN2, N = AIN3 12288 + 'AIN0': 0x4000, # Single-ended (ADS1015: AIN0 16384) + 'AIN1': 0x5000, # Single-ended (ADS1015: AIN1 20480) + 'AIN2': 0x6000, # Single-ended (ADS1015: AIN2 24576) + 'AIN3': 0x7000 # Single-ended (ADS1015: AIN3 28672) +} + +ADS1X1X_PGA = { + '6.144V': 0x0000, # +/-6.144V range = Gain 2/3 + '4.096V': 0x0200, # +/-4.096V range = Gain 1 + '2.048V': 0x0400, # +/-2.048V range = Gain 2 + '1.024V': 0x0600, # +/-1.024V range = Gain 4 + '0.512V': 0x0800, # +/-0.512V range = Gain 8 + '0.256V': 0x0A00 # +/-0.256V range = Gain 16 +} +ADS1X1X_PGA_VALUE = { + 0x0000: 6.144, + 0x0200: 4.096, + 0x0400: 2.048, + 0x0600: 1.024, + 0x0800: 0.512, + 0x0A00: 0.256, +} +ADS111X_RESOLUTION = 32767.0 +ADS111X_PGA_SCALAR = { + 0x0000: 6.144 / ADS111X_RESOLUTION, # +/-6.144V range = Gain 2/3 + 0x0200: 4.096 / ADS111X_RESOLUTION, # +/-4.096V range = Gain 1 + 0x0400: 2.048 / ADS111X_RESOLUTION, # +/-2.048V range = Gain 2 + 0x0600: 1.024 / ADS111X_RESOLUTION, # +/-1.024V range = Gain 4 + 0x0800: 0.512 / ADS111X_RESOLUTION, # +/-0.512V range = Gain 8 + 0x0A00: 0.256 / ADS111X_RESOLUTION # +/-0.256V range = Gain 16 +} +ADS101X_RESOLUTION = 2047.0 +ADS101X_PGA_SCALAR = { + 0x0000: 6.144 / ADS101X_RESOLUTION, # +/-6.144V range = Gain 2/3 + 0x0200: 4.096 / ADS101X_RESOLUTION, # +/-4.096V range = Gain 1 + 0x0400: 2.048 / ADS101X_RESOLUTION, # +/-2.048V range = Gain 2 + 0x0600: 1.024 / ADS101X_RESOLUTION, # +/-1.024V range = Gain 4 + 0x0800: 0.512 / ADS101X_RESOLUTION, # +/-0.512V range = Gain 8 + 0x0A00: 0.256 / ADS101X_RESOLUTION # +/-0.256V range = Gain 16 +} +ADS1X1X_MODE = { + 'continuous': 0x0000, # Continuous conversion mode + 'single': 0x0100 # Power-down single-shot mode +} + +# Lesser samples per second means it takes and averages more samples before +# returning a result. +ADS101X_SAMPLES_PER_SECOND = { + '128': 0x0000, # 128 samples per second + '250': 0x0020, # 250 samples per second + '490': 0x0040, # 490 samples per second + '920': 0x0060, # 920 samples per second + '1600': 0x0080, # 1600 samples per second + '2400': 0x00a0, # 2400 samples per second + '3300': 0x00c0, # 3300 samples per second +} + +ADS111X_SAMPLES_PER_SECOND = { + '8': 0x0000, # 8 samples per second + '16': 0x0020, # 16 samples per second + '32': 0x0040, # 32 samples per second + '64': 0x0060, # 64 samples per second + '128': 0x0080, # 128 samples per second + '250': 0x00a0, # 250 samples per second + '475': 0x00c0, # 475 samples per second + '860': 0x00e0 # 860 samples per second +} + +ADS1X1X_COMPARATOR_MODE = { + 'TRADITIONAL': 0x0000, # Traditional comparator with hysteresis + 'WINDOW': 0x0010 # Window comparator +} + +ADS1X1X_COMPARATOR_POLARITY = { + 'ACTIVE_LO': 0x0000, # ALERT/RDY pin is low when active + 'ACTIVE_HI': 0x0008 # ALERT/RDY pin is high when active +} + +ADS1X1X_COMPARATOR_LATCHING = { + 'NON_LATCHING': 0x0000, # Non-latching comparator + 'LATCHING': 0x0004 # Latching comparator +} + +ADS1X1X_COMPARATOR_QUEUE = { + 'QUEUE_1': 0x0000, # Assert ALERT/RDY after one conversions + 'QUEUE_2': 0x0001, # Assert ALERT/RDY after two conversions + 'QUEUE_4': 0x0002, # Assert ALERT/RDY after four conversions + 'QUEUE_NONE': 0x0003 # Disable the comparator and put ALERT/RDY + # in high state +} + +ADS1X1_OPERATIONS = { + 'SET_MUX': 0, + 'READ_CONVERSION': 1 +} + +class ADS1X1X_chip: + + def __init__(self, config): + self._printer = config.get_printer() + self._reactor = self._printer.get_reactor() + + self.name = config.get_name().split()[-1] + self.chip = config.getchoice('chip', ADS1X1X_CHIP_TYPE) + address = ADS1X1X_CHIP_ADDR['GND'] + # If none is specified, i2c_address can be used for a specific address + if config.get('address_pin', None) is not None: + address = config.getchoice('address_pin', ADS1X1X_CHIP_ADDR) + + self._ppins = self._printer.lookup_object("pins") + self._ppins.register_chip(self.name, self) + + self.pga = config.getchoice('pga', ADS1X1X_PGA, '4.096V') + self.adc_voltage = config.getfloat('adc_voltage', above=0., default=3.3) + # Comparators are not implemented, they would only be useful if the + # alert pin is used, which we haven't made configurable. + # But that wouldn't be useful for a normal temperature sensor anyway. + self.comp_mode = ADS1X1X_COMPARATOR_MODE['TRADITIONAL'] + self.comp_polarity = ADS1X1X_COMPARATOR_POLARITY['ACTIVE_LO'] + self.comp_latching = ADS1X1X_COMPARATOR_LATCHING['NON_LATCHING'] + self.comp_queue = ADS1X1X_COMPARATOR_QUEUE['QUEUE_NONE'] + self._i2c = bus.MCU_I2C_from_config(config, address) + + self.mcu = self._i2c.get_mcu() + + self._printer.add_object("ads1x1x " + self.name, self) + self._printer.register_event_handler("klippy:connect", \ + self._handle_connect) + + self._pins = {} + self._mutex = self._reactor.mutex() + + def setup_pin(self, pin_type, pin_params): + pin = pin_params['pin'] + if pin_type == 'adc': + if (pin not in ADS1X1X_MUX): + raise pins.error('ADS1x1x pin %s is not valid' % \ + pin_params['pin']) + + config = 0 + config |= (ADS1X1X_OS['OS_SINGLE'] & \ + ADS1X1X_REG_CONFIG['OS_MASK']) + config |= (ADS1X1X_MUX[pin_params['pin']] & \ + ADS1X1X_REG_CONFIG['MULTIPLEXER_MASK']) + config |= (self.pga & ADS1X1X_REG_CONFIG['PGA_MASK']) + # Have to use single mode, because in continuous, it never reaches + # idle state, which we use to determine if the sampling is done. + config |= (ADS1X1X_MODE['single'] & \ + ADS1X1X_REG_CONFIG['MODE_MASK']) + # lowest sample rate per default, until report time has been set in + # setup_adc_sample + config |= (self.comp_mode \ + & ADS1X1X_REG_CONFIG['COMPARATOR_MODE_MASK']) + config |= (self.comp_polarity \ + & ADS1X1X_REG_CONFIG['COMPARATOR_POLARITY_MASK']) + config |= (self.comp_latching \ + & ADS1X1X_REG_CONFIG['COMPARATOR_LATCHING_MASK']) + config |= (self.comp_queue \ + & ADS1X1X_REG_CONFIG['COMPARATOR_QUEUE_MASK']) + + pin_obj = ADS1X1X_pin(self, config) + if pin in self._pins: + raise pins.error( + 'pin %s for chip %s is used multiple times' \ + % (pin, self.name)) + self._pins[pin] = pin_obj + + return pin_obj + raise pins.error('Wrong pin or incompatible type: %s with type %s! ' % ( + pin, pin_type)) + + def _handle_connect(self): + try: + # Init all devices on bus for this kind of device + self._i2c.i2c_write([0x06, 0x00, 0x00]) + except Exception: + logging.exception("ADS1X1X: error while resetting device") + + def is_ready(self): + config = self._read_register(ADS1X1X_REG_POINTER['CONFIG']) + return bool((config & ADS1X1X_REG_CONFIG['OS_MASK']) == \ + ADS1X1X_OS['OS_IDLE']) + + def calculate_sample_rate(self): + pin_count = len(self._pins) + lowest_report_time = 1 + for pin in self._pins.values(): + lowest_report_time = min(lowest_report_time, pin.report_time) + + sample_rate = 1 / lowest_report_time * pin_count + samples_per_second = ADS111X_SAMPLES_PER_SECOND + if isADS101X(self.chip): + samples_per_second = ADS101X_SAMPLES_PER_SECOND + + # make sure the samples list is sorted correctly by number. + samples_per_second = sorted(samples_per_second.items(), \ + key=lambda t: int(t[0])) + for rate, bits in samples_per_second: + rate_number = int(rate) + if sample_rate <= rate_number: + return (rate_number, bits) + logging.warning( + "ADS1X1X: requested sample rate %s is higher than supported by %s."\ + % (sample_rate, self.name)) + return (rate_number, bits) + + def handle_report_time_update(self): + (sample_rate, sample_rate_bits) = self.calculate_sample_rate() + + for pin in self._pins.values(): + pin.config = (pin.config & ~ADS1X1X_REG_CONFIG['DATA_RATE_MASK']) \ + | (sample_rate_bits & ADS1X1X_REG_CONFIG['DATA_RATE_MASK']) + + self.delay = 1 / float(sample_rate) + + def sample(self, pin): + with self._mutex: + try: + self._write_register(ADS1X1X_REG_POINTER['CONFIG'], pin.config) + self._reactor.pause(self._reactor.monotonic() + self.delay) + start_time = self._reactor.monotonic() + while not self.is_ready(): + self._reactor.pause(self._reactor.monotonic() + 0.001) + # if we waited twice the expected time, mark this an error + if start_time + self.delay < self._reactor.monotonic(): + logging.warning("ADS1X1X: timeout during sampling") + return None + return self._read_register(ADS1X1X_REG_POINTER['CONVERSION']) + except Exception as e: + logging.exception("ADS1X1X: error while sampling: %s" % str(e)) + return None + + def _read_register(self, reg): + # read a single register + params = self._i2c.i2c_read([reg], 2) + buff = bytearray(params['response']) + return (buff[0]<<8 | buff[1]) + + def _write_register(self, reg, data): + data = [ + (reg & 0xFF), # Control register + ((data>>8) & 0xFF), # High byte + (data & 0xFF), # Lo byte + ] + self._i2c.i2c_write(data) + +class ADS1X1X_pin: + def __init__(self, chip, config): + self.mcu = chip.mcu + self.chip = chip + self.config = config + + self.invalid_count = 0 + + self.chip._printer.register_event_handler("klippy:connect", \ + self._handle_connect) + + def _handle_connect(self): + self._reactor = self.chip._printer.get_reactor() + self._sample_timer = \ + self._reactor.register_timer(self._process_sample, \ + self._reactor.NOW) + + def _process_sample(self, eventtime): + sample = self.chip.sample(self) + if sample is not None: + # The sample is encoded in the top 12 or full 16 bits + # Value's meaning is defined by ADS1X1X_REG_CONFIG['PGA_MASK'] + if isADS101X(self.chip.chip): + sample >>= 4 + target_value = sample / ADS101X_RESOLUTION + else: + target_value = sample / ADS111X_RESOLUTION + + # Thermistors expect a value between 0 and 1 to work. If we use a + # PGA with 4.096V but supply only 3.3V, the reference voltage for + # voltage divider is only 3.3V, not 4.096V. So we remap the range + # from what the PGA allows as range to end up between 0 and 1 for + # the thermistor logic to work as expected. + target_value = target_value * (ADS1X1X_PGA_VALUE[self.chip.pga] / \ + self.chip.adc_voltage) + + if target_value > self.maxval or target_value < self.minval: + self.invalid_count = self.invalid_count + 1 + logging.warning("ADS1X1X: temperature outside range") + self.check_invalid() + else: + self.invalid_count = 0 + + # Publish result + measured_time = self._reactor.monotonic() + self.callback(self.chip.mcu.estimated_print_time(measured_time), + target_value) + else: + self.invalid_count = self.invalid_count + 1 + self.check_invalid() + + return eventtime + self.report_time + + def check_invalid(self): + if self.invalid_count > self.range_check_count: + self.chip._printer.invoke_shutdown( + "ADS1X1X temperature check failed") + + def get_mcu(self): + return self.mcu + + def setup_adc_callback(self, report_time, callback): + self.report_time = report_time + self.callback = callback + self.chip.handle_report_time_update() + + def setup_adc_sample(self, sample_time, sample_count, + minval=0., maxval=1., range_check_count=0): + self.minval = minval + self.maxval = maxval + self.range_check_count = range_check_count + +def load_config_prefix(config): + return ADS1X1X_chip(config) diff --git a/klippy/extras/canbus_stats.py b/klippy/extras/canbus_stats.py new file mode 100644 index 000000000..0ddafaf36 --- /dev/null +++ b/klippy/extras/canbus_stats.py @@ -0,0 +1,80 @@ +# Report canbus connection status +# +# Copyright (C) 2025 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging + +class PrinterCANBusStats: + def __init__(self, config): + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + self.name = config.get_name().split()[-1] + self.mcu = None + self.get_canbus_status_cmd = None + self.status = {'rx_error': None, 'tx_error': None, 'tx_retries': None, + 'bus_state': None} + self.printer.register_event_handler("klippy:connect", + self.handle_connect) + self.printer.register_event_handler("klippy:shutdown", + self.handle_shutdown) + def handle_shutdown(self): + status = self.status.copy() + if status['bus_state'] is not None: + # Clear bus_state on shutdown to note that the values may be stale + status['bus_state'] = 'unknown' + self.status = status + def handle_connect(self): + # Lookup mcu + mcu_name = self.name + if mcu_name != 'mcu': + mcu_name = 'mcu ' + mcu_name + self.mcu = self.printer.lookup_object(mcu_name) + # Lookup status query command + if self.mcu.try_lookup_command("get_canbus_status") is None: + return + self.get_canbus_status_cmd = self.mcu.lookup_query_command( + "get_canbus_status", + "canbus_status rx_error=%u tx_error=%u tx_retries=%u" + " canbus_bus_state=%u") + # Register usb_canbus_state message handling (for usb to canbus bridge) + self.mcu.register_response(self.handle_usb_canbus_state, + "usb_canbus_state") + # Register periodic query timer + self.reactor.register_timer(self.query_event, self.reactor.NOW) + def handle_usb_canbus_state(self, params): + discard = params['discard'] + if discard: + logging.warning("USB CANBUS bridge '%s' is discarding!" + % (self.name,)) + else: + logging.warning("USB CANBUS bridge '%s' is no longer discarding." + % (self.name,)) + def query_event(self, eventtime): + prev_rx = self.status['rx_error'] + prev_tx = self.status['tx_error'] + prev_retries = self.status['tx_retries'] + if prev_rx is None: + prev_rx = prev_tx = prev_retries = 0 + params = self.get_canbus_status_cmd.send() + rx = prev_rx + ((params['rx_error'] - prev_rx) & 0xffffffff) + tx = prev_tx + ((params['tx_error'] - prev_tx) & 0xffffffff) + retries = prev_retries + ((params['tx_retries'] - prev_retries) + & 0xffffffff) + state = params['canbus_bus_state'] + self.status = {'rx_error': rx, 'tx_error': tx, 'tx_retries': retries, + 'bus_state': state} + return self.reactor.monotonic() + 1. + def stats(self, eventtime): + status = self.status + if status['rx_error'] is None: + return (False, '') + return (False, 'canstat_%s: bus_state=%s rx_error=%d' + ' tx_error=%d tx_retries=%d' + % (self.name, status['bus_state'], status['rx_error'], + status['tx_error'], status['tx_retries'])) + def get_status(self, eventtime): + return self.status + +def load_config_prefix(config): + return PrinterCANBusStats(config) diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py index 50b801412..24783c2c6 100644 --- a/klippy/extras/force_move.py +++ b/klippy/extras/force_move.py @@ -131,8 +131,12 @@ class ForceMove: x = gcmd.get_float('X', curpos[0]) y = gcmd.get_float('Y', curpos[1]) z = gcmd.get_float('Z', curpos[2]) - logging.info("SET_KINEMATIC_POSITION pos=%.3f,%.3f,%.3f", x, y, z) - toolhead.set_position([x, y, z, curpos[3]], homing_axes=(0, 1, 2)) + clear = gcmd.get('CLEAR', '').lower() + clear_axes = "".join([a for a in "xyz" if a in clear]) + logging.info("SET_KINEMATIC_POSITION pos=%.3f,%.3f,%.3f clear=%s", + x, y, z, clear_axes) + toolhead.set_position([x, y, z, curpos[3]], homing_axes="xyz") + toolhead.get_kinematics().clear_homing_state(clear_axes) def load_config(config): return ForceMove(config) diff --git a/klippy/extras/garbage_collection.py b/klippy/extras/garbage_collection.py new file mode 100644 index 000000000..da51df3b8 --- /dev/null +++ b/klippy/extras/garbage_collection.py @@ -0,0 +1,31 @@ +# Garbage collection optimizations +# +# Copyright (C) 2025 Branden Cash +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import gc +import logging + +class GarbageCollection: + def __init__(self, config): + self.printer = config.get_printer() + # feature check ... freeze/unfreeze is only available in python 3.7+ + can_freeze = hasattr(gc, 'freeze') and hasattr(gc, 'unfreeze') + if can_freeze: + self.printer.register_event_handler("klippy:ready", + self._handle_ready) + self.printer.register_event_handler("klippy:disconnect", + self._handle_disconnect) + + def _handle_ready(self): + logging.debug("Running full garbage collection and freezing") + for n in range(3): + gc.collect(n) + gc.freeze() + + def _handle_disconnect(self): + logging.debug("Unfreezing garbage collection") + gc.unfreeze() + +def load_config(config): + return GarbageCollection(config) diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index 7cb663a4a..145435427 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -14,6 +14,7 @@ KELVIN_TO_CELSIUS = -273.15 MAX_HEAT_TIME = 5.0 AMBIENT_TEMP = 25. PID_PARAM_BASE = 255. +MAX_MAINTHREAD_TIME = 5.0 class Heater: def __init__(self, config, sensor): @@ -37,7 +38,7 @@ class Heater: self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.) self.smooth_time = config.getfloat('smooth_time', 1., above=0.) self.inv_smooth_time = 1. / self.smooth_time - self.is_shutdown = False + self.verify_mainthread_time = -999. self.lock = threading.Lock() self.last_temp = self.smoothed_temp = self.target_temp = 0. self.last_temp_time = 0. @@ -66,7 +67,7 @@ class Heater: self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown) def set_pwm(self, read_time, value): - if self.target_temp <= 0. or self.is_shutdown: + if self.target_temp <= 0. or read_time > self.verify_mainthread_time: value = 0. if ((read_time < self.next_pwm_time or not self.last_pwm_value) and abs(value - self.last_pwm_value) < 0.05): @@ -91,7 +92,7 @@ class Heater: self.can_extrude = (self.smoothed_temp >= self.min_extrude_temp) #logging.debug("temp: %.3f %f = %f", read_time, temp) def _handle_shutdown(self): - self.is_shutdown = True + self.verify_mainthread_time = -999. # External commands def get_name(self): return self.name @@ -129,6 +130,9 @@ class Heater: target_temp = max(self.min_temp, min(self.max_temp, target_temp)) self.target_temp = target_temp def stats(self, eventtime): + est_print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) + if not self.printer.is_shutdown(): + self.verify_mainthread_time = est_print_time + MAX_MAINTHREAD_TIME with self.lock: target_temp = self.target_temp last_temp = self.last_temp diff --git a/klippy/extras/homing.py b/klippy/extras/homing.py index bbad53371..add209ecf 100644 --- a/klippy/extras/homing.py +++ b/klippy/extras/homing.py @@ -187,7 +187,8 @@ class Homing: # Notify of upcoming homing operation self.printer.send_event("homing:home_rails_begin", self, rails) # Alter kinematics class to think printer is at forcepos - homing_axes = [axis for axis in range(3) if forcepos[axis] is not None] + force_axes = [axis for axis in range(3) if forcepos[axis] is not None] + homing_axes = "".join(["xyz"[i] for i in force_axes]) startpos = self._fill_coord(forcepos) homepos = self._fill_coord(movepos) self.toolhead.set_position(startpos, homing_axes=homing_axes) @@ -231,7 +232,7 @@ class Homing: + self.adjust_pos.get(s.get_name(), 0.)) for s in kin.get_steppers()} newpos = kin.calc_position(kin_spos) - for axis in homing_axes: + for axis in force_axes: homepos[axis] = newpos[axis] self.toolhead.set_position(homepos) diff --git a/klippy/extras/homing_override.py b/klippy/extras/homing_override.py index e498c8b8d..0a987d59c 100644 --- a/klippy/extras/homing_override.py +++ b/klippy/extras/homing_override.py @@ -46,11 +46,11 @@ class HomingOverride: # Calculate forced position (if configured) toolhead = self.printer.lookup_object('toolhead') pos = toolhead.get_position() - homing_axes = [] + homing_axes = "" for axis, loc in enumerate(self.start_pos): if loc is not None: pos[axis] = loc - homing_axes.append(axis) + homing_axes += "xyz"[axis] toolhead.set_position(pos, homing_axes=homing_axes) # Perform homing context = self.template.create_template_context() diff --git a/klippy/extras/manual_stepper.py b/klippy/extras/manual_stepper.py index 40db4a503..9f21cc8d4 100644 --- a/klippy/extras/manual_stepper.py +++ b/klippy/extras/manual_stepper.py @@ -109,7 +109,7 @@ class ManualStepper: self.sync_print_time() def get_position(self): return [self.rail.get_commanded_position(), 0., 0., 0.] - def set_position(self, newpos, homing_axes=()): + def set_position(self, newpos, homing_axes=""): self.do_set_position(newpos[0]) def get_last_move_time(self): self.sync_print_time() diff --git a/klippy/extras/safe_z_home.py b/klippy/extras/safe_z_home.py index ca9b8eb59..32b515b9a 100644 --- a/klippy/extras/safe_z_home.py +++ b/klippy/extras/safe_z_home.py @@ -37,11 +37,10 @@ class SafeZHoming: if 'z' not in kin_status['homed_axes']: # Always perform the z_hop if the Z axis is not homed pos[2] = 0 - toolhead.set_position(pos, homing_axes=[2]) + toolhead.set_position(pos, homing_axes="z") toolhead.manual_move([None, None, self.z_hop], self.z_hop_speed) - if hasattr(toolhead.get_kinematics(), "note_z_not_homed"): - toolhead.get_kinematics().note_z_not_homed() + toolhead.get_kinematics().clear_homing_state("z") elif pos[2] < self.z_hop: # If the Z axis is homed, and below z_hop, lift it to z_hop toolhead.manual_move([None, None, self.z_hop], diff --git a/klippy/extras/save_variables.py b/klippy/extras/save_variables.py index 6cedcf466..2e405657d 100644 --- a/klippy/extras/save_variables.py +++ b/klippy/extras/save_variables.py @@ -36,6 +36,8 @@ class SaveVariables: cmd_SAVE_VARIABLE_help = "Save arbitrary variables to disk" def cmd_SAVE_VARIABLE(self, gcmd): varname = gcmd.get('VARIABLE') + if (varname.lower() != varname): + raise gcmd.error("VARIABLE must not contain upper case") value = gcmd.get('VALUE') try: value = ast.literal_eval(value) diff --git a/klippy/extras/screws_tilt_adjust.py b/klippy/extras/screws_tilt_adjust.py index 521744af3..fec54185e 100644 --- a/klippy/extras/screws_tilt_adjust.py +++ b/klippy/extras/screws_tilt_adjust.py @@ -12,7 +12,7 @@ class ScrewsTiltAdjust: self.config = config self.printer = config.get_printer() self.screws = [] - self.results = [] + self.results = {} self.max_diff = None self.max_diff_error = False # Read config diff --git a/klippy/extras/stepper_enable.py b/klippy/extras/stepper_enable.py index 621ff700e..2bad75552 100644 --- a/klippy/extras/stepper_enable.py +++ b/klippy/extras/stepper_enable.py @@ -94,6 +94,7 @@ class PrinterStepperEnable: 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): diff --git a/klippy/extras/tmc2240.py b/klippy/extras/tmc2240.py index 8816ea2e3..214896e78 100644 --- a/klippy/extras/tmc2240.py +++ b/klippy/extras/tmc2240.py @@ -408,6 +408,8 @@ class TMC2240: set_config_field(config, "tpowerdown", 10) # SG4_THRS set_config_field(config, "sg4_angle_offset", 1) + # DRV_CONF + set_config_field(config, "slope_control", 0) def load_config_prefix(config): return TMC2240(config) diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py index 0c4bb9255..2a50a05de 100644 --- a/klippy/kinematics/cartesian.py +++ b/klippy/kinematics/cartesian.py @@ -40,8 +40,6 @@ class CartKinematics: for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) - self.printer.register_event_handler("stepper_enable:motor_off", - self._motor_off) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat('max_z_velocity', max_velocity, @@ -67,15 +65,17 @@ class CartKinematics: def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - for axis in homing_axes: + for axis_name in homing_axes: + axis = "xyz".index(axis_name) if self.dc_module and axis == self.dc_module.axis: rail = self.dc_module.get_primary_rail().get_rail() else: rail = self.rails[axis] self.limits[axis] = rail.get_range() - def note_z_not_homed(self): - # Helper for Safe Z Home - self.limits[2] = (1.0, -1.0) + def clear_homing_state(self, clear_axes): + for axis, axis_name in enumerate("xyz"): + if axis_name in clear_axes: + self.limits[axis] = (1.0, -1.0) def home_axis(self, homing_state, axis, rail): # Determine movement position_min, position_max = rail.get_range() @@ -96,8 +96,6 @@ class CartKinematics: self.dc_module.home(homing_state) else: self.home_axis(homing_state, axis, self.rails[axis]) - def _motor_off(self, print_time): - self.limits = [(1.0, -1.0)] * 3 def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): diff --git a/klippy/kinematics/corexy.py b/klippy/kinematics/corexy.py index d3b755001..d68f8854f 100644 --- a/klippy/kinematics/corexy.py +++ b/klippy/kinematics/corexy.py @@ -21,8 +21,6 @@ class CoreXYKinematics: for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) - config.get_printer().register_event_handler("stepper_enable:motor_off", - self._motor_off) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( @@ -41,11 +39,12 @@ class CoreXYKinematics: def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - if i in homing_axes: + if "xyz"[i] in homing_axes: self.limits[i] = rail.get_range() - def note_z_not_homed(self): - # Helper for Safe Z Home - self.limits[2] = (1.0, -1.0) + def clear_homing_state(self, clear_axes): + for axis, axis_name in enumerate("xyz"): + if axis_name in clear_axes: + self.limits[axis] = (1.0, -1.0) def home(self, homing_state): # Each axis is homed independently and in order for axis in homing_state.get_axes(): @@ -62,8 +61,6 @@ class CoreXYKinematics: forcepos[axis] += 1.5 * (position_max - hi.position_endstop) # Perform homing homing_state.home_rails([rail], forcepos, homepos) - def _motor_off(self, print_time): - self.limits = [(1.0, -1.0)] * 3 def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): diff --git a/klippy/kinematics/corexz.py b/klippy/kinematics/corexz.py index 72134c327..8d3e027c3 100644 --- a/klippy/kinematics/corexz.py +++ b/klippy/kinematics/corexz.py @@ -21,8 +21,6 @@ class CoreXZKinematics: for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) - config.get_printer().register_event_handler("stepper_enable:motor_off", - self._motor_off) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( @@ -41,11 +39,12 @@ class CoreXZKinematics: def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - if i in homing_axes: + if "xyz"[i] in homing_axes: self.limits[i] = rail.get_range() - def note_z_not_homed(self): - # Helper for Safe Z Home - self.limits[2] = (1.0, -1.0) + def clear_homing_state(self, clear_axes): + for axis, axis_name in enumerate("xyz"): + if axis_name in clear_axes: + self.limits[axis] = (1.0, -1.0) def home(self, homing_state): # Each axis is homed independently and in order for axis in homing_state.get_axes(): @@ -62,8 +61,6 @@ class CoreXZKinematics: forcepos[axis] += 1.5 * (position_max - hi.position_endstop) # Perform homing homing_state.home_rails([rail], forcepos, homepos) - def _motor_off(self, print_time): - self.limits = [(1.0, -1.0)] * 3 def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): diff --git a/klippy/kinematics/delta.py b/klippy/kinematics/delta.py index bb81ab18a..aa244a8f3 100644 --- a/klippy/kinematics/delta.py +++ b/klippy/kinematics/delta.py @@ -23,8 +23,6 @@ class DeltaKinematics: stepper_configs[2], need_position_minmax = False, default_position_endstop=a_endstop) self.rails = [rail_a, rail_b, rail_c] - config.get_printer().register_event_handler("stepper_enable:motor_off", - self._motor_off) # Setup max velocity self.max_velocity, self.max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( @@ -90,7 +88,7 @@ class DeltaKinematics: math.sqrt(self.very_slow_xy2))) self.axes_min = toolhead.Coord(-max_xy, -max_xy, self.min_z, 0.) self.axes_max = toolhead.Coord(max_xy, max_xy, self.max_z, 0.) - self.set_position([0., 0., 0.], ()) + self.set_position([0., 0., 0.], "") def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] def _actuator_to_cartesian(self, spos): @@ -103,17 +101,19 @@ class DeltaKinematics: for rail in self.rails: rail.set_position(newpos) self.limit_xy2 = -1. - if tuple(homing_axes) == (0, 1, 2): + if homing_axes == "xyz": self.need_home = False + def clear_homing_state(self, clear_axes): + # Clearing homing state for each axis individually is not implemented + if clear_axes: + self.limit_xy2 = -1 + self.need_home = True def home(self, homing_state): # All axes are homed simultaneously homing_state.set_axes([0, 1, 2]) forcepos = list(self.home_position) forcepos[2] = -1.5 * math.sqrt(max(self.arm2)-self.max_xy2) homing_state.home_rails(self.rails, forcepos, self.home_position) - def _motor_off(self, print_time): - self.limit_xy2 = -1. - self.need_home = True def check_move(self, move): end_pos = move.end_pos end_xy2 = end_pos[0]**2 + end_pos[1]**2 diff --git a/klippy/kinematics/deltesian.py b/klippy/kinematics/deltesian.py index bb2313708..1f7ddaa05 100644 --- a/klippy/kinematics/deltesian.py +++ b/klippy/kinematics/deltesian.py @@ -41,8 +41,6 @@ class DeltesianKinematics: for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) - config.get_printer().register_event_handler( - "stepper_enable:motor_off", self._motor_off) self.limits = [(1.0, -1.0)] * 3 # X axis limits min_angle = config.getfloat('min_angle', MIN_ANGLE, @@ -89,7 +87,7 @@ class DeltesianKinematics: self.axes_min = toolhead.Coord(*[l[0] for l in self.limits], e=0.) self.axes_max = toolhead.Coord(*[l[1] for l in self.limits], e=0.) self.homed_axis = [False] * 3 - self.set_position([0., 0., 0.], ()) + self.set_position([0., 0., 0.], "") def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] def _actuator_to_cartesian(self, sp): @@ -115,8 +113,13 @@ class DeltesianKinematics: def set_position(self, newpos, homing_axes): for rail in self.rails: rail.set_position(newpos) - for n in homing_axes: - self.homed_axis[n] = True + for axis_name in homing_axes: + axis = "xyz".index(axis_name) + self.homed_axis[axis] = True + def clear_homing_state(self, clear_axes): + for axis, axis_name in enumerate("xyz"): + if axis_name in clear_axes: + self.homed_axis[axis] = False def home(self, homing_state): homing_axes = homing_state.get_axes() home_xz = 0 in homing_axes or 2 in homing_axes @@ -142,8 +145,6 @@ class DeltesianKinematics: else: forcepos[1] += 1.5 * (position_max - hi.position_endstop) homing_state.home_rails([self.rails[2]], forcepos, homepos) - def _motor_off(self, print_time): - self.homed_axis = [False] * 3 def check_move(self, move): limits = list(map(list, self.limits)) spos, epos = move.start_pos, move.end_pos diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py index 265a0e6da..fbaf49e4d 100644 --- a/klippy/kinematics/hybrid_corexy.py +++ b/klippy/kinematics/hybrid_corexy.py @@ -42,8 +42,6 @@ class HybridCoreXYKinematics: for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) - self.printer.register_event_handler("stepper_enable:motor_off", - self._motor_off) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( @@ -69,15 +67,17 @@ class HybridCoreXYKinematics: def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - for axis in homing_axes: + for axis_name in homing_axes: + axis = "xyz".index(axis_name) if self.dc_module and axis == self.dc_module.axis: rail = self.dc_module.get_primary_rail().get_rail() else: rail = self.rails[axis] self.limits[axis] = rail.get_range() - def note_z_not_homed(self): - # Helper for Safe Z Home - self.limits[2] = (1.0, -1.0) + def clear_homing_state(self, clear_axes): + for axis, axis_name in enumerate("xyz"): + if axis_name in clear_axes: + self.limits[axis] = (1.0, -1.0) def home_axis(self, homing_state, axis, rail): position_min, position_max = rail.get_range() hi = rail.get_homing_info() @@ -96,8 +96,6 @@ class HybridCoreXYKinematics: self.dc_module.home(homing_state) else: self.home_axis(homing_state, axis, self.rails[axis]) - def _motor_off(self, print_time): - self.limits = [(1.0, -1.0)] * 3 def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py index 2d89e3f7b..6fa120f77 100644 --- a/klippy/kinematics/hybrid_corexz.py +++ b/klippy/kinematics/hybrid_corexz.py @@ -42,8 +42,6 @@ class HybridCoreXZKinematics: for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) - self.printer.register_event_handler("stepper_enable:motor_off", - self._motor_off) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( @@ -69,15 +67,17 @@ class HybridCoreXZKinematics: def set_position(self, newpos, homing_axes): for i, rail in enumerate(self.rails): rail.set_position(newpos) - for axis in homing_axes: + for axis_name in homing_axes: + axis = "xyz".index(axis_name) if self.dc_module and axis == self.dc_module.axis: rail = self.dc_module.get_primary_rail().get_rail() else: rail = self.rails[axis] self.limits[axis] = rail.get_range() - def note_z_not_homed(self): - # Helper for Safe Z Home - self.limits[2] = (1.0, -1.0) + def clear_homing_state(self, clear_axes): + for axis, axis_name in enumerate("xyz"): + if axis_name in clear_axes: + self.limits[axis] = (1.0, -1.0) def home_axis(self, homing_state, axis, rail): position_min, position_max = rail.get_range() hi = rail.get_homing_info() @@ -96,8 +96,6 @@ class HybridCoreXZKinematics: self.dc_module.home(homing_state) else: self.home_axis(homing_state, axis, self.rails[axis]) - def _motor_off(self, print_time): - self.limits = [(1.0, -1.0)] * 3 def _check_endstops(self, move): end_pos = move.end_pos for i in (0, 1, 2): diff --git a/klippy/kinematics/none.py b/klippy/kinematics/none.py index ff3c57a9a..d9f9d21d2 100644 --- a/klippy/kinematics/none.py +++ b/klippy/kinematics/none.py @@ -13,6 +13,8 @@ class NoneKinematics: return [0, 0, 0] def set_position(self, newpos, homing_axes): pass + def clear_homing_state(self, clear_axes): + pass def home(self, homing_state): pass def check_move(self, move): diff --git a/klippy/kinematics/polar.py b/klippy/kinematics/polar.py index deed26d06..73967c29c 100644 --- a/klippy/kinematics/polar.py +++ b/klippy/kinematics/polar.py @@ -22,8 +22,6 @@ class PolarKinematics: for s in self.get_steppers(): s.set_trapq(toolhead.get_trapq()) toolhead.register_step_generator(s.generate_steps) - config.get_printer().register_event_handler("stepper_enable:motor_off", - self._motor_off) # Setup boundary checks max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat( @@ -47,13 +45,16 @@ class PolarKinematics: def set_position(self, newpos, homing_axes): for s in self.steppers: s.set_position(newpos) - if 2 in homing_axes: + if "z" in homing_axes: self.limit_z = self.rails[1].get_range() - if 0 in homing_axes and 1 in homing_axes: + if "x" in homing_axes and "y" in homing_axes: self.limit_xy2 = self.rails[0].get_range()[1]**2 - def note_z_not_homed(self): - # Helper for Safe Z Home - self.limit_z = (1.0, -1.0) + def clear_homing_state(self, clear_axes): + if "x" in clear_axes or "y" in clear_axes: + # X and Y cannot be cleared separately + self.limit_xy2 = -1. + if "z" in clear_axes: + self.limit_z = (1.0, -1.0) def _home_axis(self, homing_state, axis, rail): # Determine movement position_min, position_max = rail.get_range() @@ -85,9 +86,6 @@ class PolarKinematics: self._home_axis(homing_state, 0, self.rails[0]) if home_z: self._home_axis(homing_state, 2, self.rails[1]) - def _motor_off(self, print_time): - self.limit_z = (1.0, -1.0) - self.limit_xy2 = -1. def check_move(self, move): end_pos = move.end_pos xy2 = end_pos[0]**2 + end_pos[1]**2 diff --git a/klippy/kinematics/rotary_delta.py b/klippy/kinematics/rotary_delta.py index 1eb050baa..35f7ca986 100644 --- a/klippy/kinematics/rotary_delta.py +++ b/klippy/kinematics/rotary_delta.py @@ -21,8 +21,6 @@ class RotaryDeltaKinematics: stepper_configs[2], need_position_minmax=False, default_position_endstop=a_endstop, units_in_radians=True) self.rails = [rail_a, rail_b, rail_c] - config.get_printer().register_event_handler("stepper_enable:motor_off", - self._motor_off) # Read config max_velocity, max_accel = toolhead.get_max_velocity() self.max_z_velocity = config.getfloat('max_z_velocity', max_velocity, @@ -76,7 +74,7 @@ class RotaryDeltaKinematics: max_xy = math.sqrt(self.max_xy2) self.axes_min = toolhead.Coord(-max_xy, -max_xy, self.min_z, 0.) self.axes_max = toolhead.Coord(max_xy, max_xy, self.max_z, 0.) - self.set_position([0., 0., 0.], ()) + self.set_position([0., 0., 0.], "") def get_steppers(self): return [s for rail in self.rails for s in rail.get_steppers()] def calc_position(self, stepper_positions): @@ -86,8 +84,13 @@ class RotaryDeltaKinematics: for rail in self.rails: rail.set_position(newpos) self.limit_xy2 = -1. - if tuple(homing_axes) == (0, 1, 2): + if homing_axes == "xyz": self.need_home = False + def clear_homing_state(self, clear_axes): + # Clearing homing state for each axis individually is not implemented + if clear_axes: + self.limit_xy2 = -1 + self.need_home = True def home(self, homing_state): # All axes are homed simultaneously homing_state.set_axes([0, 1, 2]) @@ -96,9 +99,6 @@ class RotaryDeltaKinematics: #forcepos[2] = self.calibration.actuator_to_cartesian(min_angles)[2] forcepos[2] = -1. homing_state.home_rails(self.rails, forcepos, self.home_position) - def _motor_off(self, print_time): - self.limit_xy2 = -1. - self.need_home = True def check_move(self, move): end_pos = move.end_pos end_xy2 = end_pos[0]**2 + end_pos[1]**2 diff --git a/klippy/kinematics/winch.py b/klippy/kinematics/winch.py index 11475d24c..47bc68558 100644 --- a/klippy/kinematics/winch.py +++ b/klippy/kinematics/winch.py @@ -26,7 +26,7 @@ class WinchKinematics: acoords = list(zip(*self.anchors)) self.axes_min = toolhead.Coord(*[min(a) for a in acoords], e=0.) self.axes_max = toolhead.Coord(*[max(a) for a in acoords], e=0.) - self.set_position([0., 0., 0.], ()) + self.set_position([0., 0., 0.], "") def get_steppers(self): return list(self.steppers) def calc_position(self, stepper_positions): @@ -36,6 +36,9 @@ class WinchKinematics: def set_position(self, newpos, homing_axes): for s in self.steppers: s.set_position(newpos) + def clear_homing_state(self, clear_axes): + # XXX - homing not implemented + pass def home(self, homing_state): # XXX - homing not implemented homing_state.set_axes([0, 1, 2]) diff --git a/klippy/mcu.py b/klippy/mcu.py index eb71e6bc2..b12888f8c 100644 --- a/klippy/mcu.py +++ b/klippy/mcu.py @@ -565,6 +565,7 @@ class MCU: 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_") diff --git a/klippy/stepper.py b/klippy/stepper.py index 05e56cca4..fd44effb6 100644 --- a/klippy/stepper.py +++ b/klippy/stepper.py @@ -403,7 +403,7 @@ class PrinterRail: changed_invert = pin_params['invert'] != endstop['invert'] changed_pullup = pin_params['pullup'] != endstop['pullup'] if changed_invert or changed_pullup: - raise error("Pinter rail %s shared endstop pin %s " + raise error("Printer rail %s shared endstop pin %s " "must specify the same pullup/invert settings" % ( self.get_name(), pin_name)) mcu_endstop.add_stepper(stepper) diff --git a/klippy/toolhead.py b/klippy/toolhead.py index 256915daa..e15f987e5 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -290,7 +290,7 @@ class ToolHead: self._handle_shutdown) # Load some default modules modules = ["gcode_move", "homing", "idle_timeout", "statistics", - "manual_probe", "tuning_tower"] + "manual_probe", "tuning_tower", "garbage_collection"] for module_name in modules: self.printer.load_object(config, module_name) # Print time and flush tracking @@ -457,7 +457,7 @@ class ToolHead: # Movement commands def get_position(self): return list(self.commanded_pos) - def set_position(self, newpos, homing_axes=()): + def set_position(self, newpos, homing_axes=""): self.flush_step_generation() ffi_main, ffi_lib = chelper.get_ffi() ffi_lib.trapq_set_position(self.trapq, self.print_time, diff --git a/lib/README b/lib/README index 367e07659..a106abfd0 100644 --- a/lib/README +++ b/lib/README @@ -174,7 +174,7 @@ used to upload firmware to devices flashed with the CanBoot bootloader. The can2040 directory contains code from: https://github.com/KevinOConnor/can2040 -commit 13321ce2bc046e059a47def70f977a579a984462. +version v1.7.0 (90515f53ce89442f1bcc3033aae222e9eb77818c). The Huada HC32F460 directory contains code from: https://www.hdsc.com.cn/Category83-1490 diff --git a/lib/can2040/can2040.c b/lib/can2040/can2040.c index 4e5108ca2..f11b38184 100644 --- a/lib/can2040/can2040.c +++ b/lib/can2040/can2040.c @@ -1,6 +1,6 @@ // Software CANbus implementation for rp2040 // -// Copyright (C) 2022,2023 Kevin O'Connor +// Copyright (C) 2022-2024 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -20,6 +20,13 @@ * rp2040 and low-level helper functions ****************************************************************/ +// Determine if the target is an rp2350 +#ifdef PICO_RP2350 + #define IS_RP2350 1 +#else + #define IS_RP2350 0 +#endif + // Helper compiler definitions #define barrier() __asm__ __volatile__("": : :"memory") #define likely(x) __builtin_expect(!!(x), 1) @@ -123,19 +130,29 @@ static const uint16_t can2040_program_instructions[] = { #define SI_RX_DATA PIO_IRQ0_INTE_SM1_RXNEMPTY_BITS #define SI_TXPENDING PIO_IRQ0_INTE_SM1_BITS // Misc bit manually forced +// Return the gpio bank offset (on rp2350 chips) +static uint32_t +pio_gpiobase(struct can2040 *cd) +{ + if (!IS_RP2350) + return 0; + return (cd->gpio_rx > 31 || cd->gpio_tx > 31) ? 16 : 0; +} + // Setup PIO "sync" state machine (state machine 0) static void pio_sync_setup(struct can2040 *cd) { pio_hw_t *pio_hw = cd->pio_hw; pio_sm_hw_t *sm = &pio_hw->sm[0]; + uint32_t gpio_rx = (cd->gpio_rx - pio_gpiobase(cd)) & 0x1f; sm->execctrl = ( - cd->gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB + gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB | (can2040_offset_sync_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB | can2040_offset_sync_signal_start << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB); sm->pinctrl = ( 1 << PIO_SM0_PINCTRL_SET_COUNT_LSB - | cd->gpio_rx << PIO_SM0_PINCTRL_SET_BASE_LSB); + | gpio_rx << PIO_SM0_PINCTRL_SET_BASE_LSB); sm->instr = 0xe080; // set pindirs, 0 sm->pinctrl = 0; pio_hw->txf[0] = 9 + 6 * PIO_CLOCK_PER_BIT / 2; @@ -149,10 +166,11 @@ pio_rx_setup(struct can2040 *cd) { pio_hw_t *pio_hw = cd->pio_hw; pio_sm_hw_t *sm = &pio_hw->sm[1]; + uint32_t gpio_rx = (cd->gpio_rx - pio_gpiobase(cd)) & 0x1f; sm->execctrl = ( (can2040_offset_shared_rx_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB | can2040_offset_shared_rx_read << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB); - sm->pinctrl = cd->gpio_rx << PIO_SM0_PINCTRL_IN_BASE_LSB; + sm->pinctrl = gpio_rx << PIO_SM0_PINCTRL_IN_BASE_LSB; sm->shiftctrl = 0; // flush fifo on a restart sm->shiftctrl = (PIO_SM0_SHIFTCTRL_FJOIN_RX_BITS | PIO_RX_WAKE_BITS << PIO_SM0_SHIFTCTRL_PUSH_THRESH_LSB @@ -169,7 +187,8 @@ pio_match_setup(struct can2040 *cd) sm->execctrl = ( (can2040_offset_match_end - 1) << PIO_SM0_EXECCTRL_WRAP_TOP_LSB | can2040_offset_shared_rx_read << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB); - sm->pinctrl = cd->gpio_rx << PIO_SM0_PINCTRL_IN_BASE_LSB; + uint32_t gpio_rx = (cd->gpio_rx - pio_gpiobase(cd)) & 0x1f; + sm->pinctrl = gpio_rx << PIO_SM0_PINCTRL_IN_BASE_LSB; sm->shiftctrl = 0; sm->instr = 0xe040; // set y, 0 sm->instr = 0xa0e2; // mov osr, y @@ -183,16 +202,18 @@ pio_tx_setup(struct can2040 *cd) { pio_hw_t *pio_hw = cd->pio_hw; pio_sm_hw_t *sm = &pio_hw->sm[3]; + uint32_t gpio_rx = (cd->gpio_rx - pio_gpiobase(cd)) & 0x1f; + uint32_t gpio_tx = (cd->gpio_tx - pio_gpiobase(cd)) & 0x1f; sm->execctrl = ( - cd->gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB + gpio_rx << PIO_SM0_EXECCTRL_JMP_PIN_LSB | can2040_offset_tx_conflict << PIO_SM0_EXECCTRL_WRAP_TOP_LSB | can2040_offset_tx_conflict << PIO_SM0_EXECCTRL_WRAP_BOTTOM_LSB); sm->shiftctrl = (PIO_SM0_SHIFTCTRL_FJOIN_TX_BITS | PIO_SM0_SHIFTCTRL_AUTOPULL_BITS); sm->pinctrl = (1 << PIO_SM0_PINCTRL_SET_COUNT_LSB | 1 << PIO_SM0_PINCTRL_OUT_COUNT_LSB - | cd->gpio_tx << PIO_SM0_PINCTRL_SET_BASE_LSB - | cd->gpio_tx << PIO_SM0_PINCTRL_OUT_BASE_LSB); + | gpio_tx << PIO_SM0_PINCTRL_SET_BASE_LSB + | gpio_tx << PIO_SM0_PINCTRL_OUT_BASE_LSB); sm->instr = 0xe001; // set pins, 1 sm->instr = 0xe081; // set pindirs, 1 } @@ -382,6 +403,10 @@ pio_setup(struct can2040 *cd, uint32_t sys_clock, uint32_t bitrate) { // Configure pio0 clock uint32_t rb = cd->pio_num ? RESETS_RESET_PIO1_BITS : RESETS_RESET_PIO0_BITS; +#if IS_RP2350 + if (cd->pio_num == 2) + rb = RESETS_RESET_PIO2_BITS; +#endif rp2040_clear_reset(rb); // Setup and sync pio state machine clocks @@ -391,11 +416,16 @@ pio_setup(struct can2040 *cd, uint32_t sys_clock, uint32_t bitrate) for (i=0; i<4; i++) pio_hw->sm[i].clkdiv = div << PIO_SM0_CLKDIV_FRAC_LSB; + // Configure gpiobase (on rp2350) +#if IS_RP2350 + pio_hw->gpiobase = pio_gpiobase(cd); +#endif + // Configure state machines pio_sm_setup(cd); // Map Rx/Tx gpios - uint32_t pio_func = cd->pio_num ? 7 : 6; + uint32_t pio_func = 6 + cd->pio_num; rp2040_gpio_peripheral(cd->gpio_rx, pio_func, 1); rp2040_gpio_peripheral(cd->gpio_tx, pio_func, 0); } @@ -495,7 +525,7 @@ unstuf_restore_state(struct can2040_bitunstuffer *bu, uint32_t data) // Pull bits from unstuffer (as specified in unstuf_set_count() ) static int -unstuf_pull_bits(struct can2040_bitunstuffer *bu) +unstuf_pull_bits_rp2040(struct can2040_bitunstuffer *bu) { uint32_t sb = bu->stuffed_bits, edges = sb ^ (sb >> 1); uint32_t e2 = edges | (edges >> 1), e4 = e2 | (e2 >> 2), rm_bits = ~e4; @@ -539,6 +569,49 @@ unstuf_pull_bits(struct can2040_bitunstuffer *bu) } } +// Pull bits from unstuffer (optimized for rp2350) +static int +unstuf_pull_bits(struct can2040_bitunstuffer *bu) +{ + if (!IS_RP2350) + return unstuf_pull_bits_rp2040(bu); + uint32_t sb = bu->stuffed_bits, edges = sb ^ (sb >> 1); + uint32_t e2 = edges | (edges >> 1), e4 = e2 | (e2 >> 2), rm_bits = ~e4; + uint32_t cs = bu->count_stuff, cu = bu->count_unstuff; + for (;;) { + if (!cs) + // Need more data + return 1; + uint32_t try_cnt = cs > cu ? cu : cs; + uint32_t try_mask = ((1 << try_cnt) - 1) << (cs + 1 - try_cnt); + uint32_t rm_masked_bits = rm_bits & try_mask; + if (likely(!rm_masked_bits)) { + // No stuff bits in try_cnt bits - copy into unstuffed_bits + bu->count_unstuff = cu = cu - try_cnt; + bu->count_stuff = cs = cs - try_cnt; + bu->unstuffed_bits |= ((sb >> cs) & ((1 << try_cnt) - 1)) << cu; + if (! cu) + // Extracted desired bits + return 0; + // Need more data + return 1; + } + // Copy any leading bits prior to stuff bit (may be zero) + uint32_t copy_cnt = cs - (31 - __builtin_clz(rm_masked_bits)); + cs -= copy_cnt; + bu->count_unstuff = cu = cu - copy_cnt; + bu->unstuffed_bits |= ((sb >> cs) & ((1 << copy_cnt) - 1)) << cu; + // High bit is now a stuff bit - remove it + bu->count_stuff = cs = cs - 1; + if (unlikely(rm_bits & (1 << cs))) { + // Six consecutive bits - a bitstuff error + if (sb & (1 << cs)) + return -1; + return -2; + } + } +} + // Return most recent raw (still stuffed) bits static uint32_t unstuf_get_raw(struct can2040_bitunstuffer *bu) @@ -553,7 +626,7 @@ unstuf_get_raw(struct can2040_bitunstuffer *bu) // Stuff 'num_bits' bits in '*pb' - upper bits must already be stuffed static uint32_t -bitstuff(uint32_t *pb, uint32_t num_bits) +bitstuff_rp2040(uint32_t *pb, uint32_t num_bits) { uint32_t b = *pb, count = num_bits; for (;;) { @@ -590,6 +663,34 @@ done: return count; } +// Stuff 'num_bits' bits in '*pb' (optimized for rp2350) +static uint32_t +bitstuff(uint32_t *pb, uint32_t num_bits) +{ + if (!IS_RP2350) + return bitstuff_rp2040(pb, num_bits); + uint32_t b = *pb, count = num_bits; + for (;;) { + uint32_t edges = b ^ (b >> 1); + uint32_t e2 = edges | (edges >> 1), e4 = e2 | (e2 >> 2), add_bits = ~e4; + uint32_t mask = (1 << num_bits) - 1, add_masked_bits = add_bits & mask; + if (!add_masked_bits) + // No more stuff bits needed + break; + // Insert a stuff bit + uint32_t stuff_pos = 1 + 31 - __builtin_clz(add_masked_bits); + uint32_t low_mask = (1 << stuff_pos) - 1, low = b & low_mask; + uint32_t high = (b & ~(low_mask >> 1)) << 1; + b = high ^ low ^ (1 << (stuff_pos - 1)); + count += 1; + if (stuff_pos <= 4) + break; + num_bits = stuff_pos - 4; + } + *pb = b; + return count; +} + // State storage for building bit stuffed transmit messages struct bitstuffer_s { uint32_t prev_stuffed, bitpos, *buf; @@ -1326,6 +1427,12 @@ can2040_setup(struct can2040 *cd, uint32_t pio_num) memset(cd, 0, sizeof(*cd)); cd->pio_num = !!pio_num; cd->pio_hw = cd->pio_num ? pio1_hw : pio0_hw; +#if IS_RP2350 + if (pio_num == 2) { + cd->pio_num = pio_num; + cd->pio_hw = pio2_hw; + } +#endif } // API function to configure callback diff --git a/scripts/klippy-requirements.txt b/scripts/klippy-requirements.txt index 7fb235404..87e664c6b 100644 --- a/scripts/klippy-requirements.txt +++ b/scripts/klippy-requirements.txt @@ -9,3 +9,4 @@ greenlet==3.0.3 ; python_version >= '3.12' Jinja2==2.11.3 python-can==3.3.4 markupsafe==1.1.1 +setuptools==75.6.0 ; python_version >= '3.12' # Needed by python-can diff --git a/src/atsam/fdcan.c b/src/atsam/fdcan.c index deeb60706..20c86b443 100644 --- a/src/atsam/fdcan.c +++ b/src/atsam/fdcan.c @@ -1,11 +1,12 @@ // CANbus support on atsame70 chips // -// Copyright (C) 2021-2022 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // Copyright (C) 2019 Eug Krashtan // Copyright (C) 2020 Pontus Borg // // This file may be distributed under the terms of the GNU GPLv3 license. +#include "board/irq.h" // irq_save #include "command.h" // DECL_CONSTANT_STR #include "generic/armcm_boot.h" // armcm_enable_irq #include "generic/canbus.h" // canbus_notify_tx @@ -147,6 +148,38 @@ canhw_set_filter(uint32_t id) CANx->MCAN_CCCR &= ~MCAN_CCCR_INIT; } +static struct { + uint32_t rx_error, tx_error; +} CAN_Errors; + +// Report interface status +void +canhw_get_status(struct canbus_status *status) +{ + irqstatus_t flag = irq_save(); + uint32_t psr = CANx->MCAN_PSR, lec = psr & MCAN_PSR_LEC_Msk; + if (lec && lec != 7) { + // Reading PSR clears it - so update state here + if (lec >= 3 && lec <= 5) + CAN_Errors.tx_error += 1; + else + CAN_Errors.rx_error += 1; + } + uint32_t rx_error = CAN_Errors.rx_error, tx_error = CAN_Errors.tx_error; + irq_restore(flag); + + status->rx_error = rx_error; + status->tx_error = tx_error; + if (psr & MCAN_PSR_BO) + status->bus_state = CANBUS_STATE_OFF; + else if (psr & MCAN_PSR_EP) + status->bus_state = CANBUS_STATE_PASSIVE; + else if (psr & MCAN_PSR_EW) + status->bus_state = CANBUS_STATE_WARN; + else + status->bus_state = 0; +} + // This function handles CAN global interrupts void CAN_IRQHandler(void) @@ -183,6 +216,18 @@ CAN_IRQHandler(void) CANx->MCAN_IR = FDCAN_IE_TC; canbus_notify_tx(); } + if (ir & (MCAN_IR_PED | MCAN_IR_PEA)) { + // Bus error + uint32_t psr = CANx->MCAN_PSR; + CANx->MCAN_IR = MCAN_IR_PED | MCAN_IR_PEA; + uint32_t lec = psr & MCAN_PSR_LEC_Msk; + if (lec && lec != 7) { + if (lec >= 3 && lec <= 5) + CAN_Errors.tx_error += 1; + else + CAN_Errors.rx_error += 1; + } + } } static inline const uint32_t @@ -302,6 +347,6 @@ can_init(void) /*##-3- Configure Interrupts #################################*/ armcm_enable_irq(CAN_IRQHandler, CANx_IRQn, 1); CANx->MCAN_ILE = MCAN_ILE_EINT0; - CANx->MCAN_IE = MCAN_IE_RF0NE | FDCAN_IE_TC; + CANx->MCAN_IE = MCAN_IE_RF0NE | FDCAN_IE_TC | MCAN_IE_PEDE | MCAN_IE_PEAE; } DECL_INIT(can_init); diff --git a/src/atsamd/fdcan.c b/src/atsamd/fdcan.c index 5ad62c0f7..e8c107654 100644 --- a/src/atsamd/fdcan.c +++ b/src/atsamd/fdcan.c @@ -1,11 +1,12 @@ // CANbus support on atsame51 chips // -// Copyright (C) 2021-2022 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // Copyright (C) 2019 Eug Krashtan // Copyright (C) 2020 Pontus Borg // // This file may be distributed under the terms of the GNU GPLv3 license. +#include "board/irq.h" // irq_save #include "command.h" // DECL_CONSTANT_STR #include "generic/armcm_boot.h" // armcm_enable_irq #include "generic/canbus.h" // canbus_notify_tx @@ -163,6 +164,38 @@ canhw_set_filter(uint32_t id) CANx->CCCR.reg &= ~CAN_CCCR_INIT; } +static struct { + uint32_t rx_error, tx_error; +} CAN_Errors; + +// Report interface status +void +canhw_get_status(struct canbus_status *status) +{ + irqstatus_t flag = irq_save(); + uint32_t psr = CANx->PSR.reg, lec = psr & CAN_PSR_LEC_Msk; + if (lec && lec != 7) { + // Reading PSR clears it - so update state here + if (lec >= 3 && lec <= 5) + CAN_Errors.tx_error += 1; + else + CAN_Errors.rx_error += 1; + } + uint32_t rx_error = CAN_Errors.rx_error, tx_error = CAN_Errors.tx_error; + irq_restore(flag); + + status->rx_error = rx_error; + status->tx_error = tx_error; + if (psr & CAN_PSR_BO) + status->bus_state = CANBUS_STATE_OFF; + else if (psr & CAN_PSR_EP) + status->bus_state = CANBUS_STATE_PASSIVE; + else if (psr & CAN_PSR_EW) + status->bus_state = CANBUS_STATE_WARN; + else + status->bus_state = 0; +} + // This function handles CAN global interrupts void CAN_IRQHandler(void) @@ -199,6 +232,18 @@ CAN_IRQHandler(void) CANx->IR.reg = FDCAN_IE_TC; canbus_notify_tx(); } + if (ir & (CAN_IR_PED | CAN_IR_PEA)) { + // Bus error + uint32_t psr = CANx->PSR.reg; + CANx->IR.reg = CAN_IR_PED | CAN_IR_PEA; + uint32_t lec = psr & CAN_PSR_LEC_Msk; + if (lec && lec != 7) { + if (lec >= 3 && lec <= 5) + CAN_Errors.tx_error += 1; + else + CAN_Errors.rx_error += 1; + } + } } static inline const uint32_t @@ -309,6 +354,6 @@ can_init(void) /*##-3- Configure Interrupts #################################*/ armcm_enable_irq(CAN_IRQHandler, CANx_IRQn, 1); CANx->ILE.reg = CAN_ILE_EINT0; - CANx->IE.reg = CAN_IE_RF0NE | FDCAN_IE_TC; + CANx->IE.reg = CAN_IE_RF0NE | FDCAN_IE_TC | CAN_IE_PEDE | CAN_IE_PEAE; } DECL_INIT(can_init); diff --git a/src/generic/canbus.h b/src/generic/canbus.h index f21a7cc45..a0a2c2abe 100644 --- a/src/generic/canbus.h +++ b/src/generic/canbus.h @@ -17,9 +17,20 @@ struct canbus_msg { #define CANMSG_DATA_LEN(msg) ((msg)->dlc > 8 ? 8 : (msg)->dlc) +struct canbus_status { + uint32_t rx_error, tx_error, tx_retries; + uint32_t bus_state; +}; + +enum { + CANBUS_STATE_ACTIVE, CANBUS_STATE_WARN, CANBUS_STATE_PASSIVE, + CANBUS_STATE_OFF, +}; + // callbacks provided by board specific code int canhw_send(struct canbus_msg *msg); void canhw_set_filter(uint32_t id); +void canhw_get_status(struct canbus_status *status); // canbus.c int canbus_send(struct canbus_msg *msg); diff --git a/src/generic/canserial.c b/src/generic/canserial.c index d56235f57..b90eb6591 100644 --- a/src/generic/canserial.c +++ b/src/generic/canserial.c @@ -2,7 +2,7 @@ // // Copyright (C) 2019 Eug Krashtan // Copyright (C) 2020 Pontus Borg -// Copyright (C) 2021 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -318,6 +318,25 @@ DECL_TASK(canserial_rx_task); * Setup and shutdown ****************************************************************/ +DECL_ENUMERATION("canbus_bus_state", "active", CANBUS_STATE_ACTIVE); +DECL_ENUMERATION("canbus_bus_state", "warn", CANBUS_STATE_WARN); +DECL_ENUMERATION("canbus_bus_state", "passive", CANBUS_STATE_PASSIVE); +DECL_ENUMERATION("canbus_bus_state", "off", CANBUS_STATE_OFF); + +void +command_get_canbus_status(uint32_t *args) +{ + struct canbus_status status; + memset(&status, 0, sizeof(status)); + canhw_get_status(&status); + sendf("canbus_status rx_error=%u tx_error=%u tx_retries=%u" + " canbus_bus_state=%u" + , status.rx_error, status.tx_error, status.tx_retries + , status.bus_state); +} +DECL_COMMAND_FLAGS(command_get_canbus_status, HF_IN_SHUTDOWN + , "get_canbus_status"); + void command_get_canbus_id(uint32_t *args) { diff --git a/src/generic/usb_canbus.c b/src/generic/usb_canbus.c index 9c2893bf2..25d41c1be 100644 --- a/src/generic/usb_canbus.c +++ b/src/generic/usb_canbus.c @@ -1,6 +1,6 @@ // Support for Linux "gs_usb" CANbus adapter emulation // -// Copyright (C) 2018-2022 Kevin O'Connor +// Copyright (C) 2018-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -108,6 +108,10 @@ static struct usbcan_data { uint8_t notify_local, usb_send_busy; uint32_t assigned_id; + // State tracking for messages to be sent from host to canbus + uint32_t bus_send_discard_time; + uint8_t bus_send_state; + // Canbus data from host uint8_t host_status; uint32_t host_pull_pos, host_push_pos; @@ -118,6 +122,10 @@ static struct usbcan_data { struct canbus_msg canhw_queue[32]; } UsbCan; +enum { + BSS_READY = 0, BSS_BLOCKING, BSS_DISCARDING +}; + enum { HS_TX_ECHO = 1, HS_TX_HW = 2, @@ -205,10 +213,56 @@ fill_usb_host_queue(void) } } +// Report bus stall state +static void +note_discard_state(uint32_t discard) +{ + sendf("usb_canbus_state discard=%u", discard); +} + +// Check if canbus queue has gotten stuck +static int +check_need_discard(void) +{ + if (UsbCan.bus_send_state != BSS_BLOCKING) + return 0; + return timer_is_before(UsbCan.bus_send_discard_time, timer_read_time()); +} + +// Attempt to send a message on the canbus +static int +try_canmsg_send(struct canbus_msg *msg) +{ + int ret = canhw_send(msg); + if (ret >= 0) { + // Success + if (UsbCan.bus_send_state == BSS_DISCARDING) + note_discard_state(0); + UsbCan.bus_send_state = BSS_READY; + return ret; + } + + // Unable to send message + if (check_need_discard()) { + // The canbus is stalled - start discarding messages + note_discard_state(1); + UsbCan.bus_send_state = BSS_DISCARDING; + } + if (UsbCan.bus_send_state == BSS_DISCARDING) + // Queue is stalled - just discard the message + return 0; + if (UsbCan.bus_send_state == BSS_READY) { + // Just starting to block - setup stall detection after 50ms + UsbCan.bus_send_state = BSS_BLOCKING; + UsbCan.bus_send_discard_time = timer_read_time() + timer_from_us(50000); + } + return ret; +} + void usbcan_task(void) { - if (!sched_check_wake(&UsbCan.wake)) + if (!sched_check_wake(&UsbCan.wake) && !check_need_discard()) return; // Send any pending hw frames to host @@ -235,7 +289,7 @@ usbcan_task(void) UsbCan.host_status = host_status = host_status & ~HS_TX_LOCAL; } if (host_status & HS_TX_HW) { - int ret = canhw_send(&msg); + int ret = try_canmsg_send(&msg); if (ret < 0) break; UsbCan.host_status = host_status = host_status & ~HS_TX_HW; diff --git a/src/rp2040/can.c b/src/rp2040/can.c index 1f4c281ec..0e62ac3b2 100644 --- a/src/rp2040/can.c +++ b/src/rp2040/can.c @@ -1,6 +1,6 @@ // Serial over CAN emulation for rp2040 using can2040 software canbus // -// Copyright (C) 2022 Kevin O'Connor +// Copyright (C) 2022-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -41,6 +41,23 @@ canhw_set_filter(uint32_t id) // Filter not implemented (and not necessary) } +static uint32_t last_tx_retries; + +// Report interface status +void +canhw_get_status(struct canbus_status *status) +{ + struct can2040_stats stats; + can2040_get_statistics(&cbus, &stats); + uint32_t tx_extra = stats.tx_attempt - stats.tx_total; + if (last_tx_retries != tx_extra) + last_tx_retries = tx_extra - 1; + + status->rx_error = stats.parse_error; + status->tx_retries = last_tx_retries; + status->bus_state = CANBUS_STATE_ACTIVE; +} + // can2040 callback function - handle rx and tx notifications static void can2040_cb(struct can2040 *cd, uint32_t notify, struct can2040_msg *msg) diff --git a/src/stm32/can.c b/src/stm32/can.c index dc9153282..6d35e677b 100644 --- a/src/stm32/can.c +++ b/src/stm32/can.c @@ -2,7 +2,7 @@ // // Copyright (C) 2019 Eug Krashtan // Copyright (C) 2020 Pontus Borg -// Copyright (C) 2021 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -168,6 +168,31 @@ canhw_set_filter(uint32_t id) fcan->FMR = fmr & ~CAN_FMR_FINIT; } +static struct { + uint32_t rx_error, tx_error; +} CAN_Errors; + +// Report interface status +void +canhw_get_status(struct canbus_status *status) +{ + irqstatus_t flag = irq_save(); + uint32_t esr = SOC_CAN->ESR; + uint32_t rx_error = CAN_Errors.rx_error, tx_error = CAN_Errors.tx_error; + irq_restore(flag); + + status->rx_error = rx_error; + status->tx_error = tx_error; + if (esr & CAN_ESR_BOFF) + status->bus_state = CANBUS_STATE_OFF; + else if (esr & CAN_ESR_EPVF) + status->bus_state = CANBUS_STATE_PASSIVE; + else if (esr & CAN_ESR_EWGF) + status->bus_state = CANBUS_STATE_WARN; + else + status->bus_state = 0; +} + // This function handles CAN global interrupts void CAN_IRQHandler(void) @@ -190,6 +215,8 @@ CAN_IRQHandler(void) // Process packet canbus_process_data(&msg); } + + // Check for transmit ready uint32_t ier = SOC_CAN->IER; if (ier & CAN_IER_TMEIE && SOC_CAN->TSR & (CAN_TSR_RQCP0|CAN_TSR_RQCP1|CAN_TSR_RQCP2)) { @@ -197,6 +224,21 @@ CAN_IRQHandler(void) SOC_CAN->IER = ier & ~CAN_IER_TMEIE; canbus_notify_tx(); } + + // Check for error irq + uint32_t msr = SOC_CAN->MSR; + if (msr & CAN_MSR_ERRI) { + uint32_t esr = SOC_CAN->ESR; + uint32_t lec = (esr & CAN_ESR_LEC_Msk) >> CAN_ESR_LEC_Pos; + if (lec && lec != 7) { + SOC_CAN->ESR = 7 << CAN_ESR_LEC_Pos; + if (lec >= 3 && lec <= 5) + CAN_Errors.tx_error += 1; + else + CAN_Errors.rx_error += 1; + } + SOC_CAN->MSR = CAN_MSR_ERRI; + } } static inline const uint32_t @@ -289,6 +331,8 @@ can_init(void) armcm_enable_irq(CAN_IRQHandler, CAN_RX1_IRQn, 0); if (CAN_RX0_IRQn != CAN_TX_IRQn) armcm_enable_irq(CAN_IRQHandler, CAN_TX_IRQn, 0); - SOC_CAN->IER = CAN_IER_FMPIE0; + if (CAN_RX0_IRQn != CAN_SCE_IRQn) + armcm_enable_irq(CAN_IRQHandler, CAN_SCE_IRQn, 0); + SOC_CAN->IER = CAN_IER_FMPIE0 | CAN_IER_ERRIE; } DECL_INIT(can_init); diff --git a/src/stm32/fdcan.c b/src/stm32/fdcan.c index 5344d26b1..8a2d1ab2a 100644 --- a/src/stm32/fdcan.c +++ b/src/stm32/fdcan.c @@ -1,11 +1,12 @@ // FDCAN support on stm32 chips // -// Copyright (C) 2021-2022 Kevin O'Connor +// Copyright (C) 2021-2025 Kevin O'Connor // Copyright (C) 2019 Eug Krashtan // Copyright (C) 2020 Pontus Borg // // This file may be distributed under the terms of the GNU GPLv3 license. +#include "board/irq.h" // irq_save #include "command.h" // DECL_CONSTANT_STR #include "generic/armcm_boot.h" // armcm_enable_irq #include "generic/canbus.h" // canbus_notify_tx @@ -184,6 +185,38 @@ canhw_set_filter(uint32_t id) SOC_CAN->CCCR &= ~FDCAN_CCCR_INIT; } +static struct { + uint32_t rx_error, tx_error; +} CAN_Errors; + +// Report interface status +void +canhw_get_status(struct canbus_status *status) +{ + irqstatus_t flag = irq_save(); + uint32_t psr = SOC_CAN->PSR, lec = psr & FDCAN_PSR_LEC_Msk; + if (lec && lec != 7) { + // Reading PSR clears it - so update state here + if (lec >= 3 && lec <= 5) + CAN_Errors.tx_error += 1; + else + CAN_Errors.rx_error += 1; + } + uint32_t rx_error = CAN_Errors.rx_error, tx_error = CAN_Errors.tx_error; + irq_restore(flag); + + status->rx_error = rx_error; + status->tx_error = tx_error; + if (psr & FDCAN_PSR_BO) + status->bus_state = CANBUS_STATE_OFF; + else if (psr & FDCAN_PSR_EP) + status->bus_state = CANBUS_STATE_PASSIVE; + else if (psr & FDCAN_PSR_EW) + status->bus_state = CANBUS_STATE_WARN; + else + status->bus_state = 0; +} + // This function handles CAN global interrupts void CAN_IRQHandler(void) @@ -220,6 +253,18 @@ CAN_IRQHandler(void) SOC_CAN->IR = FDCAN_IE_TC; canbus_notify_tx(); } + if (ir & (FDCAN_IR_PED | FDCAN_IR_PEA)) { + // Bus error + uint32_t psr = SOC_CAN->PSR; + SOC_CAN->IR = FDCAN_IR_PED | FDCAN_IR_PEA; + uint32_t lec = psr & FDCAN_PSR_LEC_Msk; + if (lec && lec != 7) { + if (lec >= 3 && lec <= 5) + CAN_Errors.tx_error += 1; + else + CAN_Errors.rx_error += 1; + } + } } static inline const uint32_t @@ -320,6 +365,6 @@ can_init(void) /*##-3- Configure Interrupts #################################*/ armcm_enable_irq(CAN_IRQHandler, CAN_IT0_IRQn, 1); SOC_CAN->ILE = FDCAN_ILE_EINT0; - SOC_CAN->IE = FDCAN_IE_RF0NE | FDCAN_IE_TC; + SOC_CAN->IE = FDCAN_IE_RF0NE | FDCAN_IE_TC | FDCAN_IE_PEDE | FDCAN_IE_PEAE; } DECL_INIT(can_init); diff --git a/src/stm32/gpio.h b/src/stm32/gpio.h index 78f567b81..96105e9bc 100644 --- a/src/stm32/gpio.h +++ b/src/stm32/gpio.h @@ -38,7 +38,13 @@ void gpio_adc_cancel_sample(struct gpio_adc g); struct spi_config { void *spi; - uint32_t spi_cr1; + union { + uint32_t spi_cr1; + struct { + uint8_t div; + uint8_t mode; + }; + }; }; struct spi_config spi_setup(uint32_t bus, uint8_t mode, uint32_t rate); void spi_prepare(struct spi_config config); diff --git a/src/stm32/stm32h7_spi.c b/src/stm32/stm32h7_spi.c index ec69f851a..35f25e46a 100644 --- a/src/stm32/stm32h7_spi.c +++ b/src/stm32/stm32h7_spi.c @@ -100,19 +100,27 @@ spi_setup(uint32_t bus, uint8_t mode, uint32_t rate) while ((pclk >> (div + 1)) > rate && div < 7) div++; - uint32_t cr1 = SPI_CR1_SPE; spi->CFG1 |= (div << SPI_CFG1_MBR_Pos) | (7 << SPI_CFG1_DSIZE_Pos); CLEAR_BIT(spi->CFG1, SPI_CFG1_CRCSIZE); spi->CFG2 |= ((mode << SPI_CFG2_CPHA_Pos) | SPI_CFG2_MASTER | SPI_CFG2_SSM | SPI_CFG2_AFCNTR | SPI_CFG2_SSOE); spi->CR1 |= SPI_CR1_SSI; - return (struct spi_config){ .spi = spi, .spi_cr1 = cr1 }; + return (struct spi_config){ .spi = spi, .div = div, .mode = mode }; } void spi_prepare(struct spi_config config) { + uint32_t div = config.div; + uint32_t mode = config.mode; + SPI_TypeDef *spi = config.spi; + // Reload frequency + spi->CFG1 = (spi->CFG1 & ~SPI_CFG1_MBR_Msk); + spi->CFG1 |= (div << SPI_CFG1_MBR_Pos); + // Reload mode + spi->CFG2 = (spi->CFG2 & ~SPI_CFG2_CPHA_Msk); + spi->CFG2 |= (mode << SPI_CFG2_CPHA_Pos); } void diff --git a/test/klippy/printers.test b/test/klippy/printers.test index 40a6118c0..92c340cb8 100644 --- a/test/klippy/printers.test +++ b/test/klippy/printers.test @@ -40,6 +40,7 @@ CONFIG ../../config/printer-creality-cr20-pro-2019.cfg CONFIG ../../config/printer-creality-ender5plus-2019.cfg CONFIG ../../config/printer-eryone-thinker-series-v2-2020.cfg CONFIG ../../config/printer-flashforge-creator-pro-2018.cfg +CONFIG ../../config/printer-geeetech-A10T-A20T-2021.cfg CONFIG ../../config/printer-hiprecy-leo-2019.cfg CONFIG ../../config/printer-longer-lk4-pro-2019.cfg CONFIG ../../config/printer-lulzbot-mini1-2016.cfg